From 38d7a60d0009c6733ff482269a518065124228ae Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Tue, 6 May 2025 18:08:14 +0200 Subject: [PATCH] cli: add commands to inspect the tree --- src/cli.rs | 6 +- src/cli/tree.rs | 419 ++++++++++++++++++ src/ifs.rs | 1 + src/ifs/jay_compositor.rs | 8 + src/ifs/jay_tree_query.rs | 458 ++++++++++++++++++++ src/ifs/wl_surface/xdg_surface.rs | 6 + src/ifs/wl_surface/xdg_surface/xdg_popup.rs | 4 + src/ifs/wl_surface/zwlr_layer_surface_v1.rs | 6 + src/tools/tool_client.rs | 54 ++- src/tree/placeholder.rs | 4 +- src/tree/stacked.rs | 4 + src/tree/toplevel.rs | 4 +- wire/jay_compositor.txt | 4 + wire/jay_tree_query.txt | 102 +++++ 14 files changed, 1072 insertions(+), 8 deletions(-) create mode 100644 src/cli/tree.rs create mode 100644 src/ifs/jay_tree_query.rs create mode 100644 wire/jay_tree_query.txt diff --git a/src/cli.rs b/src/cli.rs index 2ebdcd47..cc31a642 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -14,6 +14,7 @@ mod run_privileged; pub mod screenshot; mod seat_test; mod set_log_level; +mod tree; mod unlock; mod xwayland; @@ -22,7 +23,7 @@ use { cli::{ clients::ClientsArgs, color_management::ColorManagementArgs, damage_tracking::DamageTrackingArgs, idle::IdleCmd, input::InputArgs, randr::RandrArgs, - reexec::ReexecArgs, xwayland::XwaylandArgs, + reexec::ReexecArgs, tree::TreeArgs, xwayland::XwaylandArgs, }, compositor::start_compositor, format::{Format, ref_formats}, @@ -89,6 +90,8 @@ pub enum Cmd { Reexec(ReexecArgs), /// Inspect/manipulate the connected clients. Clients(ClientsArgs), + /// Inspect the surface tree. + Tree(TreeArgs), #[cfg(feature = "it")] RunTests, } @@ -248,6 +251,7 @@ pub fn main() { Cmd::Xwayland(a) => xwayland::main(cli.global, a), Cmd::ColorManagement(a) => color_management::main(cli.global, a), Cmd::Clients(a) => clients::main(cli.global, a), + Cmd::Tree(a) => tree::main(cli.global, a), #[cfg(feature = "it")] Cmd::RunTests => crate::it::run_tests(), Cmd::Reexec(a) => reexec::main(cli.global, a), diff --git a/src/cli/tree.rs b/src/cli/tree.rs new file mode 100644 index 00000000..f751a8e5 --- /dev/null +++ b/src/cli/tree.rs @@ -0,0 +1,419 @@ +use { + crate::{ + cli::{ + GlobalArgs, + clients::{Client, ClientPrinter, handle_client_query}, + }, + ifs::jay_tree_query::{ + TREE_TY_CONTAINER, TREE_TY_DISPLAY, TREE_TY_FLOAT, TREE_TY_LAYER_SURFACE, + TREE_TY_LOCK_SURFACE, TREE_TY_OUTPUT, TREE_TY_PLACEHOLDER, TREE_TY_WORKSPACE, + TREE_TY_X_WINDOW, TREE_TY_XDG_POPUP, TREE_TY_XDG_TOPLEVEL, + }, + rect::Rect, + tools::tool_client::{Handle, ToolClient, with_tool_client}, + wire::{JayCompositorId, JayTreeQueryId, jay_client_query, jay_compositor, jay_tree_query}, + }, + ahash::{AHashMap, AHashSet}, + clap::{Args, Subcommand}, + isnt::std_1::primitive::IsntSliceExt, + std::{cell::RefCell, rc::Rc}, +}; + +#[derive(Args, Debug)] +pub struct TreeArgs { + #[clap(subcommand)] + cmd: TreeCmd, +} + +#[derive(Subcommand, Debug)] +enum TreeCmd { + /// Query the tree. + Query(QueryArgs), +} + +#[derive(Args, Debug)] +struct QueryArgs { + /// Whether to perform a recursive query. + #[arg(short, long)] + recursive: bool, + /// Whether to repeatedly print details of the same client. + #[arg(long)] + all_clients: bool, + #[clap(subcommand)] + cmd: QueryCmd, +} + +#[derive(Subcommand, Debug)] +enum QueryCmd { + /// Query the entire display. + Root, + /// Query a workspace by name. + WorkspaceName(QueryWorkspaceNameArgs), + /// Interactively select a workspace to query. + SelectWorkspace, + /// Interactively select a window to query. + SelectWindow, +} + +#[derive(Args, Debug)] +struct QueryWorkspaceNameArgs { + /// The name of the workspace. + name: String, +} + +pub fn main(global: GlobalArgs, tree_args: TreeArgs) { + with_tool_client(global.log_level.into(), |tc| async move { + let comp = tc.jay_compositor().await; + let tree = Rc::new(Tree { + tc: tc.clone(), + comp, + }); + tree.run(tree_args).await; + }); +} + +struct Tree { + tc: Rc, + comp: JayCompositorId, +} + +impl Tree { + async fn run(&self, args: TreeArgs) { + match &args.cmd { + TreeCmd::Query(a) => self.query(a).await, + } + } + + async fn query(&self, args: &QueryArgs) { + let id = self.tc.id(); + self.tc.send(jay_compositor::CreateTreeQuery { + self_id: self.comp, + id, + }); + let mut query = Query { + tree: self, + tc: &self.tc, + id, + }; + query.run(args).await; + } +} + +struct Query<'a> { + tree: &'a Tree, + tc: &'a Rc, + id: JayTreeQueryId, +} + +#[derive(Debug, Default)] +struct Queried { + not_found: bool, + roots: Vec, + stack: Vec, + client_ids: AHashSet, +} + +#[derive(Debug, Default)] +struct Node { + ty: u32, + children: Vec, + position: Option, + toplevel_id: Option, + client: Option, + title: Option, + app_id: Option, + tag: Option, + x_class: Option, + x_instance: Option, + x_role: Option, + workspace: Option, + placeholder_for: Option, + floating: bool, + visible: bool, + urgent: bool, + fullscreen: bool, + output: Option, +} + +impl Query<'_> { + async fn run(&mut self, args: &QueryArgs) { + match &args.cmd { + QueryCmd::Root => { + self.tc.send(SetRootDisplay { self_id: self.id }); + } + QueryCmd::WorkspaceName(a) => { + self.tc.send(SetRootWorkspaceName { + self_id: self.id, + workspace: &a.name, + }); + } + QueryCmd::SelectWorkspace => { + let id = self.tc.select_workspace().await; + if id.is_none() { + fatal!("Workspace selection failed"); + } + self.tc.send(SetRootWorkspace { + self_id: self.id, + workspace: id, + }); + } + QueryCmd::SelectWindow => { + let id = self.tc.select_toplevel().await; + if id.is_none() { + fatal!("Window selection failed"); + } + self.tc.send(SetRootToplevel { + self_id: self.id, + toplevel: id, + }); + } + } + let tl = self.tc; + let id = self.id; + let d = Rc::new(RefCell::new(Queried::default())); + use jay_tree_query::*; + macro_rules! last { + ($d:ident, $n:ident) => { + let $d = &mut *$d.borrow_mut(); + let $n = $d.stack.last_mut().unwrap(); + }; + } + NotFound::handle(tl, id, d.clone(), |d, _event| { + d.borrow_mut().not_found = true; + }); + End::handle(tl, id, d.clone(), |d, _event| { + let d = &mut *d.borrow_mut(); + let n = d.stack.pop().unwrap(); + if let Some(p) = d.stack.last_mut() { + p.children.push(n); + } else { + d.roots.push(n); + } + }); + Position::handle(tl, id, d.clone(), |d, event| { + last!(d, n); + n.position = Rect::new_sized(event.x, event.y, event.w, event.h); + }); + Start::handle(tl, id, d.clone(), |d, event| { + let d = &mut *d.borrow_mut(); + let node = Node { + ty: event.ty, + ..Default::default() + }; + d.stack.push(node); + }); + OutputName::handle(tl, id, d.clone(), |d, event| { + last!(d, n); + n.output = Some(event.name.to_string()); + }); + WorkspaceName::handle(tl, id, d.clone(), |d, event| { + last!(d, n); + n.workspace = Some(event.name.to_string()); + }); + ToplevelId::handle(tl, id, d.clone(), |d, event| { + last!(d, n); + n.toplevel_id = Some(event.id.to_string()); + }); + ClientId::handle(tl, id, d.clone(), |d, event| { + last!(d, n); + n.client = Some(event.id); + d.client_ids.insert(event.id); + }); + Title::handle(tl, id, d.clone(), |d, event| { + last!(d, n); + n.title = Some(event.title.to_string()); + }); + AppId::handle(tl, id, d.clone(), |d, event| { + last!(d, n); + n.app_id = Some(event.app_id.to_string()); + }); + Floating::handle(tl, id, d.clone(), |d, _event| { + last!(d, n); + n.floating = true; + }); + Visible::handle(tl, id, d.clone(), |d, _event| { + last!(d, n); + n.visible = true; + }); + Urgent::handle(tl, id, d.clone(), |d, _event| { + last!(d, n); + n.urgent = true; + }); + Fullscreen::handle(tl, id, d.clone(), |d, _event| { + last!(d, n); + n.fullscreen = true; + }); + Tag::handle(tl, id, d.clone(), |d, event| { + last!(d, n); + n.tag = Some(event.tag.to_string()); + }); + XClass::handle(tl, id, d.clone(), |d, event| { + last!(d, n); + n.x_class = Some(event.class.to_string()); + }); + XInstance::handle(tl, id, d.clone(), |d, event| { + last!(d, n); + n.x_instance = Some(event.instance.to_string()); + }); + XRole::handle(tl, id, d.clone(), |d, event| { + last!(d, n); + n.x_role = Some(event.role.to_string()); + }); + Workspace::handle(tl, id, d.clone(), |d, event| { + last!(d, n); + n.workspace = Some(event.name.to_string()); + }); + PlaceholderFor::handle(tl, id, d.clone(), |d, event| { + last!(d, n); + n.placeholder_for = Some(event.id.to_string()); + }); + if args.recursive { + tl.send(SetRecursive { + self_id: id, + recursive: 1, + }); + } + tl.send(Execute { self_id: id }); + tl.round_trip().await; + let clients = { + let id = tl.id(); + tl.send(jay_compositor::CreateClientQuery { + self_id: self.tree.comp, + id, + }); + use jay_client_query::*; + for &client in &d.borrow().client_ids { + tl.send(AddId { + self_id: id, + id: client, + }); + } + tl.send(Execute { self_id: id }); + handle_client_query(tl, id).await + }; + let mut printer = Printer { + clients, + printed_clients: Default::default(), + verbose: args.all_clients, + prefix: "".to_string(), + output_depth: 0, + workspace_depth: 0, + }; + for node in &d.borrow().roots { + printer.print(node); + } + } +} + +struct Printer { + clients: AHashMap, + printed_clients: AHashSet, + verbose: bool, + prefix: String, + output_depth: u32, + workspace_depth: u32, +} + +impl Printer { + fn print(&mut self, node: &Node) { + let p = &self.prefix; + 'ty: { + let n = match node.ty { + TREE_TY_DISPLAY => "display", + TREE_TY_OUTPUT => "output", + TREE_TY_WORKSPACE => "workspace", + TREE_TY_FLOAT => "float", + TREE_TY_CONTAINER => "container", + TREE_TY_PLACEHOLDER => "placeholder", + TREE_TY_XDG_TOPLEVEL => "xdg-toplevel", + TREE_TY_X_WINDOW => "x-window", + TREE_TY_XDG_POPUP => "xdg-popup", + TREE_TY_LAYER_SURFACE => "layer-surface", + TREE_TY_LOCK_SURFACE => "lock-surface", + _ => { + println!("{p}- unknown ({}):", node.ty); + break 'ty; + } + }; + println!("{p}- {n}:"); + } + macro_rules! opt { + ($field:ident, $pretty:expr) => { + if let Some(v) = &node.$field { + println!("{p} {}: {}", $pretty, v); + } + }; + } + macro_rules! bol { + ($field:ident, $pretty:expr) => { + if node.$field { + println!("{p} {}", $pretty); + } + }; + } + if node.ty == TREE_TY_OUTPUT { + opt!(output, "name"); + } + if node.ty == TREE_TY_WORKSPACE { + opt!(workspace, "name"); + } + opt!(toplevel_id, "id"); + opt!(placeholder_for, "placeholder-for"); + if let Some(r) = node.position { + println!( + "{p} pos: {}x{} + {}x{}", + r.x1(), + r.y1(), + r.width(), + r.height() + ); + } + if let Some(client_id) = node.client { + let client = self.clients.get(&client_id); + if client.is_some() && (self.printed_clients.insert(client_id) || self.verbose) { + println!("{p} client:"); + let mut prefix = format!("{} ", p); + let mut cp = ClientPrinter { + prefix: &mut prefix, + }; + cp.print_client(client.unwrap()); + } else { + println!("{p} client: {client_id}"); + } + } + opt!(title, "title"); + opt!(app_id, "app-id"); + opt!(tag, "tag"); + opt!(x_class, "x-class"); + opt!(x_instance, "x-instance"); + opt!(x_role, "x-role"); + if self.workspace_depth == 0 && node.ty != TREE_TY_WORKSPACE { + opt!(workspace, "workspace"); + } + if self.workspace_depth == 0 && self.output_depth == 0 && node.ty != TREE_TY_OUTPUT { + opt!(output, "output"); + } + bol!(floating, "floating"); + bol!(visible, "visible"); + bol!(urgent, "urgent"); + bol!(fullscreen, "fullscreen"); + if node.children.is_not_empty() { + let (od, wd) = match node.ty { + TREE_TY_OUTPUT => (1, 0), + TREE_TY_WORKSPACE => (0, 1), + _ => (0, 0), + }; + self.output_depth += od; + self.workspace_depth += wd; + println!("{p} children:"); + let len = self.prefix.len(); + self.prefix.push_str(" "); + for child in &node.children { + self.print(child); + } + self.prefix.truncate(len); + self.output_depth -= od; + self.workspace_depth -= wd; + } + } +} diff --git a/src/ifs.rs b/src/ifs.rs index 0fb8d916..1d50c259 100644 --- a/src/ifs.rs +++ b/src/ifs.rs @@ -31,6 +31,7 @@ pub mod jay_select_toplevel; pub mod jay_select_workspace; pub mod jay_toplevel; pub mod jay_tray_v1; +pub mod jay_tree_query; pub mod jay_workspace; pub mod jay_workspace_watcher; pub mod jay_xwayland; diff --git a/src/ifs/jay_compositor.rs b/src/ifs/jay_compositor.rs index b8372e89..70e4e31d 100644 --- a/src/ifs/jay_compositor.rs +++ b/src/ifs/jay_compositor.rs @@ -20,6 +20,7 @@ use { jay_seat_events::JaySeatEvents, jay_select_toplevel::{JaySelectToplevel, JayToplevelSelector}, jay_select_workspace::{JaySelectWorkspace, JayWorkspaceSelector}, + jay_tree_query::JayTreeQuery, jay_workspace_watcher::JayWorkspaceWatcher, jay_xwayland::JayXwayland, }, @@ -507,6 +508,13 @@ impl JayCompositorRequestHandler for JayCompositor { self.client.state.clients.kill(ClientId::from_raw(req.id)); Ok(()) } + + fn create_tree_query(&self, req: CreateTreeQuery, _slf: &Rc) -> Result<(), Self::Error> { + let obj = Rc::new(JayTreeQuery::new(&self.client, req.id, self.version)); + track!(self.client, obj); + self.client.add_client_obj(&obj)?; + Ok(()) + } } object_base! { diff --git a/src/ifs/jay_tree_query.rs b/src/ifs/jay_tree_query.rs new file mode 100644 index 00000000..1171a89c --- /dev/null +++ b/src/ifs/jay_tree_query.rs @@ -0,0 +1,458 @@ +use { + crate::{ + client::{Client, ClientError}, + globals::GlobalBase, + ifs::wl_surface::{ + ext_session_lock_surface_v1::ExtSessionLockSurfaceV1, + x_surface::xwindow::Xwindow, + xdg_surface::{xdg_popup::XdgPopup, xdg_toplevel::XdgToplevel}, + zwlr_layer_surface_v1::ZwlrLayerSurfaceV1, + }, + leaks::Tracker, + object::{Object, Version}, + rect::Rect, + tree::{ + self, ContainerNode, DisplayNode, FloatNode, Node, NodeVisitor, OutputNode, + PlaceholderNode, ToplevelData, ToplevelNodeBase, ToplevelType, WorkspaceNode, + }, + utils::{opaque::OpaqueError, opt::Opt, toplevel_identifier::ToplevelIdentifier}, + wire::{JayTreeQueryId, jay_tree_query::*}, + }, + isnt::std_1::primitive::IsntStrExt, + std::{ + cell::{Cell, RefCell}, + ops::Deref, + rc::Rc, + str::FromStr, + }, + thiserror::Error, +}; + +pub const TREE_TY_DISPLAY: u32 = 1; +pub const TREE_TY_OUTPUT: u32 = 2; +pub const TREE_TY_WORKSPACE: u32 = 3; +pub const TREE_TY_FLOAT: u32 = 4; +pub const TREE_TY_CONTAINER: u32 = 5; +pub const TREE_TY_PLACEHOLDER: u32 = 6; +pub const TREE_TY_XDG_TOPLEVEL: u32 = 7; +pub const TREE_TY_X_WINDOW: u32 = 8; +pub const TREE_TY_XDG_POPUP: u32 = 9; +pub const TREE_TY_LAYER_SURFACE: u32 = 10; +pub const TREE_TY_LOCK_SURFACE: u32 = 11; + +pub struct JayTreeQuery { + pub id: JayTreeQueryId, + pub client: Rc, + pub tracker: Tracker, + pub version: Version, + recursive: Cell, + root: RefCell>, +} + +enum Root { + Display, + WorkspaceNode(Rc>), + WorkspaceName(String), + ToplevelId(ToplevelIdentifier), +} + +impl JayTreeQuery { + pub fn new(client: &Rc, id: JayTreeQueryId, version: Version) -> Self { + Self { + id, + client: client.clone(), + tracker: Default::default(), + version, + recursive: Cell::new(false), + root: Default::default(), + } + } + + fn send_node_position(&self, node: &dyn Node) { + let rect = node.node_absolute_position(); + self.send_position(rect); + } + + fn send_position(&self, rect: Rect) { + self.client.event(Position { + self_id: self.id, + x: rect.x1(), + y: rect.y1(), + w: rect.width(), + h: rect.height(), + }); + } + + fn send_not_found(&self) { + self.client.event(NotFound { self_id: self.id }); + } + + fn send_done(&self) { + self.client.event(Done { self_id: self.id }); + } + + fn send_end(&self) { + self.client.event(End { self_id: self.id }); + } + + fn send_start(&self, ty: u32) { + self.client.event(Start { + self_id: self.id, + ty, + }); + } + + fn send_client(&self, node: &impl Node) { + if let Some(id) = node.node_client_id() { + self.client.event(ClientId { + self_id: self.id, + id: id.raw(), + }); + } + } + + fn send_workspace_name(&self, name: &str) { + self.client.event(WorkspaceName { + self_id: self.id, + name, + }); + } + + fn send_output_name(&self, name: &str) { + self.client.event(OutputName { + self_id: self.id, + name, + }); + } + + fn send_toplevel(&self, data: &ToplevelData) { + self.client.event(Start { + self_id: self.id, + ty: match &data.kind { + ToplevelType::Container => TREE_TY_CONTAINER, + ToplevelType::Placeholder(_) => TREE_TY_PLACEHOLDER, + ToplevelType::XdgToplevel(_) => TREE_TY_XDG_TOPLEVEL, + ToplevelType::XWindow(_) => TREE_TY_X_WINDOW, + }, + }); + self.client.event(ToplevelId { + self_id: self.id, + id: &data.identifier.get().to_string(), + }); + self.send_position(data.desired_extents.get()); + if let Some(cl) = data.client.as_ref().map(|c| c.id.raw()) { + self.client.event(ClientId { + self_id: self.id, + id: cl, + }); + } + self.client.event(Title { + self_id: self.id, + title: &data.title.borrow(), + }); + if let Some(w) = data.workspace.get() { + self.send_workspace_name(&w.name); + } + match &data.kind { + ToplevelType::Container => {} + ToplevelType::Placeholder(id) => { + if let Some(id) = *id { + self.client.event(PlaceholderFor { + self_id: self.id, + id: &id.to_string(), + }); + } + } + ToplevelType::XdgToplevel(d) => { + self.client.event(AppId { + self_id: self.id, + app_id: &data.app_id.borrow(), + }); + let tag = &*d.tag.borrow(); + if tag.is_not_empty() { + self.client.event(Tag { + self_id: self.id, + tag, + }); + } + } + ToplevelType::XWindow(d) => { + if let Some(class) = &*d.info.class.borrow() { + self.client.event(XClass { + self_id: self.id, + class, + }); + } + if let Some(instance) = &*d.info.instance.borrow() { + self.client.event(XInstance { + self_id: self.id, + instance, + }); + } + if let Some(role) = &*d.info.role.borrow() { + self.client.event(XRole { + self_id: self.id, + role, + }); + } + } + } + if data.is_floating.get() { + self.client.event(Floating { self_id: self.id }); + } + if data.visible.get() { + self.client.event(Visible { self_id: self.id }); + } + if data.wants_attention.get() { + self.client.event(Urgent { self_id: self.id }); + } + for seat_id in data.seat_foci.lock().keys() { + for seat in data.state.globals.seats.lock().values() { + if seat.id() == *seat_id { + self.client.event(Focused { + self_id: self.id, + global: seat.name().raw(), + }); + } + } + } + if data.is_fullscreen.get() { + self.client.event(Fullscreen { self_id: self.id }); + } + if let Some(ws) = data.workspace.get() { + self.client.event(Workspace { + self_id: self.id, + name: &ws.name, + }); + } + } +} + +impl JayTreeQueryRequestHandler for JayTreeQuery { + type Error = JayTreeQueryError; + + fn destroy(&self, _req: Destroy, _slf: &Rc) -> Result<(), Self::Error> { + self.client.remove_obj(self)?; + Ok(()) + } + + fn execute(&self, _req: Execute, _slf: &Rc) -> Result<(), Self::Error> { + let Some(root) = &*self.root.borrow() else { + return Err(JayTreeQueryError::NoRootSet); + }; + match root { + Root::Display => Visitor(self).visit_display(&self.client.state.root), + Root::WorkspaceNode(n) => match n.get() { + Some(n) => Visitor(self).visit_workspace(&n), + None => self.send_not_found(), + }, + Root::WorkspaceName(n) => match self.client.state.workspaces.get(n) { + Some(n) => Visitor(self).visit_workspace(&n), + None => self.send_not_found(), + }, + Root::ToplevelId(id) => match self + .client + .state + .toplevels + .get(id) + .and_then(|t| t.upgrade()) + { + Some(t) => t.node_visit(&mut Visitor(self)), + None => self.send_not_found(), + }, + } + self.send_done(); + Ok(()) + } + + fn set_root_display(&self, _req: SetRootDisplay, _slf: &Rc) -> Result<(), Self::Error> { + *self.root.borrow_mut() = Some(Root::Display); + Ok(()) + } + + fn set_recursive(&self, req: SetRecursive, _slf: &Rc) -> Result<(), Self::Error> { + self.recursive.set(req.recursive != 0); + Ok(()) + } + + fn set_root_workspace( + &self, + req: SetRootWorkspace, + _slf: &Rc, + ) -> Result<(), Self::Error> { + let ws = self.client.lookup(req.workspace)?; + let opt = match ws.workspace.get() { + Some(ws) => ws.opt.clone(), + _ => Default::default(), + }; + let root = &mut *self.root.borrow_mut(); + *root = Some(Root::WorkspaceNode(opt)); + Ok(()) + } + + fn set_root_workspace_name( + &self, + req: SetRootWorkspaceName, + _slf: &Rc, + ) -> Result<(), Self::Error> { + let root = &mut *self.root.borrow_mut(); + *root = Some(Root::WorkspaceName(req.workspace.to_owned())); + Ok(()) + } + + fn set_root_toplevel(&self, req: SetRootToplevel, _slf: &Rc) -> Result<(), Self::Error> { + let tl = self.client.lookup(req.toplevel)?; + let root = &mut *self.root.borrow_mut(); + *root = Some(Root::ToplevelId(tl.toplevel.tl_data().identifier.get())); + Ok(()) + } + + fn set_root_window_id( + &self, + req: SetRootWindowId<'_>, + _slf: &Rc, + ) -> Result<(), Self::Error> { + let id = + ToplevelIdentifier::from_str(req.id).map_err(JayTreeQueryError::InvalidToplevelId)?; + let root = &mut *self.root.borrow_mut(); + *root = Some(Root::ToplevelId(id)); + Ok(()) + } +} + +struct Visitor<'a>(&'a JayTreeQuery); + +impl tree::NodeVisitorBase for Visitor<'_> { + fn visit_container(&mut self, node: &Rc) { + let s = self.0; + s.send_toplevel(node.tl_data()); + if s.recursive.get() { + node.node_visit_children(self); + } + s.send_end(); + } + + fn visit_toplevel(&mut self, node: &Rc) { + let s = self.0; + s.send_toplevel(node.tl_data()); + node.xdg.for_each_popup(|popup| { + NodeVisitor::visit_popup(self, popup); + }); + s.send_end(); + } + + fn visit_popup(&mut self, node: &Rc) { + let s = self.0; + s.send_start(TREE_TY_XDG_POPUP); + s.send_node_position(&**node); + s.send_end(); + } + + fn visit_display(&mut self, node: &Rc) { + let s = self.0; + s.send_start(TREE_TY_DISPLAY); + s.send_node_position(&**node); + if s.recursive.get() { + for output in node.outputs.lock().values() { + NodeVisitor::visit_output(self, output); + } + for stacked in node.stacked.iter() { + if stacked.stacked_has_workspace_link() { + continue; + } + stacked.deref().clone().node_visit(self); + } + } + s.send_end(); + } + + fn visit_output(&mut self, node: &Rc) { + let s = self.0; + s.send_start(TREE_TY_OUTPUT); + s.send_node_position(&**node); + s.send_output_name(&node.global.connector.name); + if s.recursive.get() { + node.node_visit_children(self); + } + s.send_end(); + } + + fn visit_float(&mut self, node: &Rc) { + let s = self.0; + s.send_start(TREE_TY_FLOAT); + s.send_node_position(&**node); + if s.recursive.get() { + node.node_visit_children(self); + } + s.send_end(); + } + + fn visit_workspace(&mut self, node: &Rc) { + let s = self.0; + s.send_start(TREE_TY_WORKSPACE); + s.send_node_position(&**node); + s.send_workspace_name(&node.name); + s.send_output_name(&node.output.get().global.connector.name); + for stacked in node.stacked.iter() { + if stacked.stacked_is_xdg_popup() { + continue; + } + stacked.deref().clone().node_visit(self); + } + if s.recursive.get() { + node.node_visit_children(self); + } + s.send_end(); + } + + fn visit_layer_surface(&mut self, node: &Rc) { + let s = self.0; + s.send_start(TREE_TY_LAYER_SURFACE); + s.send_client(&**node); + s.send_node_position(&**node); + node.for_each_popup(|popup| { + NodeVisitor::visit_popup(self, popup); + }); + s.send_end(); + } + + fn visit_xwindow(&mut self, node: &Rc) { + let s = self.0; + s.send_toplevel(node.tl_data()); + s.send_end(); + } + + fn visit_placeholder(&mut self, node: &Rc) { + let s = self.0; + s.send_toplevel(node.tl_data()); + s.send_end(); + } + + fn visit_lock_surface(&mut self, node: &Rc) { + let s = self.0; + s.send_start(TREE_TY_LOCK_SURFACE); + s.send_client(&**node); + s.send_node_position(&**node); + s.send_end(); + } +} + +object_base! { + self = JayTreeQuery; + version = self.version; +} + +impl Object for JayTreeQuery {} + +simple_add_obj!(JayTreeQuery); + +#[derive(Debug, Error)] +pub enum JayTreeQueryError { + #[error(transparent)] + ClientError(Box), + #[error("Toplevel id is ill-formed")] + InvalidToplevelId(OpaqueError), + #[error("No root node was set")] + NoRootSet, +} +efrom!(JayTreeQueryError, ClientError); diff --git a/src/ifs/wl_surface/xdg_surface.rs b/src/ifs/wl_surface/xdg_surface.rs index d0b9e433..9477c92c 100644 --- a/src/ifs/wl_surface/xdg_surface.rs +++ b/src/ifs/wl_surface/xdg_surface.rs @@ -328,6 +328,12 @@ impl XdgSurface { popup.popup.xdg.set_popup_stack(stack); } } + + pub fn for_each_popup(&self, mut f: impl FnMut(&Rc)) { + for popup in self.popups.lock().values() { + f(&popup.popup); + } + } } impl XdgSurfaceRequestHandler for XdgSurface { diff --git a/src/ifs/wl_surface/xdg_surface/xdg_popup.rs b/src/ifs/wl_surface/xdg_surface/xdg_popup.rs index 2fc5685e..7544b529 100644 --- a/src/ifs/wl_surface/xdg_surface/xdg_popup.rs +++ b/src/ifs/wl_surface/xdg_surface/xdg_popup.rs @@ -382,6 +382,10 @@ impl StackedNode for XdgPopup { fn stacked_absolute_position_constrains_input(&self) -> bool { false } + + fn stacked_is_xdg_popup(&self) -> bool { + true + } } impl XdgSurfaceExt for XdgPopup { diff --git a/src/ifs/wl_surface/zwlr_layer_surface_v1.rs b/src/ifs/wl_surface/zwlr_layer_surface_v1.rs index 3c759a29..0da67246 100644 --- a/src/ifs/wl_surface/zwlr_layer_surface_v1.rs +++ b/src/ifs/wl_surface/zwlr_layer_surface_v1.rs @@ -210,6 +210,12 @@ impl ZwlrLayerSurfaceV1 { m.layer_surface.get_or_insert_default_ext() }) } + + pub fn for_each_popup(&self, mut f: impl FnMut(&Rc)) { + for popup in self.popups.lock().values() { + f(&popup.popup); + } + } } impl ZwlrLayerSurfaceV1RequestHandler for ZwlrLayerSurfaceV1 { diff --git a/src/tools/tool_client.rs b/src/tools/tool_client.rs index 09751475..995d240e 100644 --- a/src/tools/tool_client.rs +++ b/src/tools/tool_client.rs @@ -23,9 +23,10 @@ use { }, wheel::{Wheel, WheelError}, wire::{ - JayCompositor, JayCompositorId, JayDamageTracking, JayDamageTrackingId, WlCallbackId, - WlRegistryId, WlSeatId, jay_compositor, jay_select_toplevel, jay_toplevel, wl_callback, - wl_display, wl_registry, + JayCompositor, JayCompositorId, JayDamageTracking, JayDamageTrackingId, JayToplevelId, + JayWorkspaceId, WlCallbackId, WlRegistryId, WlSeatId, jay_compositor, + jay_select_toplevel, jay_select_workspace, jay_toplevel, wl_callback, wl_display, + wl_registry, }, }, ahash::AHashMap, @@ -362,6 +363,53 @@ impl ToolClient { Some(id) } + pub async fn select_workspace(self: &Rc) -> JayWorkspaceId { + let id = self.id(); + self.send(jay_compositor::SelectWorkspace { + self_id: self.jay_compositor().await, + id, + seat: WlSeatId::NONE, + }); + let ae = Rc::new(AsyncEvent::default()); + let ws = Rc::new(Cell::new(JayWorkspaceId::NONE)); + jay_select_workspace::Cancelled::handle(self, id, ae.clone(), |ae, _event| { + ae.trigger(); + }); + jay_select_workspace::Selected::handle( + self, + id, + (ae.clone(), ws.clone()), + |(ae, ws), event| { + ws.set(event.id); + ae.trigger(); + }, + ); + ae.triggered().await; + ws.get() + } + + pub async fn select_toplevel(self: &Rc) -> JayToplevelId { + let id = self.id(); + self.send(jay_compositor::SelectToplevel { + self_id: self.jay_compositor().await, + id, + seat: WlSeatId::NONE, + }); + let ae = Rc::new(AsyncEvent::default()); + let toplevel = Rc::new(Cell::new(JayToplevelId::NONE)); + jay_select_toplevel::Done::handle( + self, + id, + (ae.clone(), toplevel.clone()), + |(ae, toplevel), event| { + toplevel.set(event.id); + ae.trigger(); + }, + ); + ae.triggered().await; + toplevel.get() + } + pub async fn select_toplevel_client(self: &Rc) -> u64 { let id = self.id(); self.send(jay_compositor::SelectToplevel { diff --git a/src/tree/placeholder.rs b/src/tree/placeholder.rs index 1dd3ee3b..fbf3362c 100644 --- a/src/tree/placeholder.rs +++ b/src/tree/placeholder.rs @@ -56,7 +56,7 @@ impl PlaceholderNode { state, node.tl_data().title.borrow().clone(), node.node_client(), - ToplevelType::Placeholder, + ToplevelType::Placeholder(Some(node.tl_data().identifier.get())), id, slf, ), @@ -75,7 +75,7 @@ impl PlaceholderNode { state, String::new(), None, - ToplevelType::Placeholder, + ToplevelType::Placeholder(None), id, slf, ), diff --git a/src/tree/stacked.rs b/src/tree/stacked.rs index 4b745005..92caf4d6 100644 --- a/src/tree/stacked.rs +++ b/src/tree/stacked.rs @@ -16,6 +16,10 @@ pub trait StackedNode: Node { fn stacked_absolute_position_constrains_input(&self) -> bool { true } + + fn stacked_is_xdg_popup(&self) -> bool { + false + } } pub trait PinnedNode: StackedNode { diff --git a/src/tree/toplevel.rs b/src/tree/toplevel.rs index adcef45e..161dc3e5 100644 --- a/src/tree/toplevel.rs +++ b/src/tree/toplevel.rs @@ -279,7 +279,7 @@ impl ToplevelOpt { pub enum ToplevelType { Container, - Placeholder, + Placeholder(Option), XdgToplevel(Rc), XWindow(Rc), } @@ -288,7 +288,7 @@ impl ToplevelType { pub fn to_window_type(&self) -> WindowType { match self { ToplevelType::Container => window::CONTAINER, - ToplevelType::Placeholder => window::PLACEHOLDER, + ToplevelType::Placeholder { .. } => window::PLACEHOLDER, ToplevelType::XdgToplevel { .. } => window::XDG_TOPLEVEL, ToplevelType::XWindow { .. } => window::X_WINDOW, } diff --git a/wire/jay_compositor.txt b/wire/jay_compositor.txt index cecbe1f0..1a4657a8 100644 --- a/wire/jay_compositor.txt +++ b/wire/jay_compositor.txt @@ -117,6 +117,10 @@ request kill_client (since = 18) { id: pod(u64), } +request create_tree_query (since = 18) { + id: id(jay_tree_query), +} + # events event client_id { diff --git a/wire/jay_tree_query.txt b/wire/jay_tree_query.txt new file mode 100644 index 00000000..2919ba46 --- /dev/null +++ b/wire/jay_tree_query.txt @@ -0,0 +1,102 @@ +request destroy { } + +request execute { } + +request set_root_display { } + +request set_recursive { + recursive: u32, +} + +request set_root_workspace { + workspace: id(jay_workspace), +} + +request set_root_workspace_name { + workspace: str, +} + +request set_root_toplevel { + toplevel: id(jay_toplevel), +} + +request set_root_window_id { + id: str, +} + +event done { } + +event not_found { } + +event start { + ty: u32, +} + +event end { } + +event position { + x: i32, + y: i32, + w: i32, + h: i32, +} + +event workspace_name { + name: str, +} + +event output_name { + name: str, +} + +event toplevel_id { + id: str, +} + +event client_id { + id: pod(u64), +} + +event title { + title: str, +} + +event app_id { + app_id: str, +} + +event floating { } + +event visible { } + +event urgent { } + +event focused { + global: u32, +} + +event fullscreen { } + +event tag { + tag: str, +} + +event x_class { + class: str, +} + +event x_instance { + instance: str, +} + +event x_role { + role: str, +} + +event workspace { + name: str, +} + +event placeholder_for { + id: str, +}