use { crate::{ client::{Client, ClientId}, control_center::{ CcBehavior, ControlCenterInner, PaneType, cc_criterion::{CcCriterion, CritImpl, CritRegex}, cc_window::show_window_collapsible, grid, icon_label, label, read_only_bool, }, criteria::{CritMgrExt, CritUpstreamNode, crit_leaf::CritLeafMatcher}, egui_adapter::egui_platform::icons::ICON_OPEN_IN_NEW, state::State, tree::ToplevelData, utils::{ copyhashmap::CopyHashMap, static_text::StaticText, toplevel_identifier::ToplevelIdentifier, }, }, ahash::AHashMap, egui::{ CollapsingHeader, DragValue, Sense, TextFormat, Ui, Widget, cache::CacheTrait, text::LayoutJob, }, linearize::Linearize, std::{ any::Any, rc::{Rc, Weak}, }, }; pub enum ClientCrit { SandboxEngine(CritRegex), SandboxAppId(CritRegex), SandboxInstanceId(CritRegex), Sandboxed, Uid(i32), Pid(i32), IsXwayland, Comm(CritRegex), Exe(CritRegex), Tag(CritRegex), } #[derive(Copy, Clone, Debug, Eq, PartialEq, Linearize)] pub enum ClientCritTy { SandboxEngine, SandboxAppId, SandboxInstanceId, Sandboxed, Uid, Pid, IsXwayland, Comm, Exe, Tag, } impl Default for ClientCrit { fn default() -> Self { ClientCrit::Comm(Default::default()) } } impl StaticText for ClientCritTy { fn text(&self) -> &'static str { match self { ClientCritTy::SandboxEngine => "Sandbox Engine", ClientCritTy::SandboxAppId => "Sandbox App ID", ClientCritTy::SandboxInstanceId => "Sandbox Instance ID", ClientCritTy::Sandboxed => "Sandboxed", ClientCritTy::Uid => "UID", ClientCritTy::Pid => "PID", ClientCritTy::IsXwayland => "Is Xwayland", ClientCritTy::Comm => "Comm", ClientCritTy::Exe => "Exe", ClientCritTy::Tag => "Tag", } } } impl CritImpl for ClientCrit { type Type = ClientCritTy; type Target = Rc; fn ty(&self) -> Self::Type { macro_rules! map { ($($n:ident,)*) => { match self { $( Self::$n { .. } => ClientCritTy::$n, )* } }; } map! { SandboxEngine, SandboxAppId, SandboxInstanceId, Sandboxed, Uid, Pid, IsXwayland, Comm, Exe, Tag, } } fn from_ty(ty: Self::Type) -> Self { match ty { ClientCritTy::SandboxEngine => Self::SandboxEngine(Default::default()), ClientCritTy::SandboxAppId => Self::SandboxAppId(Default::default()), ClientCritTy::SandboxInstanceId => Self::SandboxInstanceId(Default::default()), ClientCritTy::Sandboxed => Self::Sandboxed, ClientCritTy::Uid => Self::Uid(Default::default()), ClientCritTy::Pid => Self::Pid(Default::default()), ClientCritTy::IsXwayland => Self::IsXwayland, ClientCritTy::Comm => Self::Comm(Default::default()), ClientCritTy::Exe => Self::Exe(Default::default()), ClientCritTy::Tag => Self::Tag(Default::default()), } } fn show(&mut self, ui: &mut Ui) -> bool { match self { ClientCrit::SandboxEngine(v) => v.show(ui), ClientCrit::SandboxAppId(v) => v.show(ui), ClientCrit::SandboxInstanceId(v) => v.show(ui), ClientCrit::Sandboxed => false, ClientCrit::Uid(v) => DragValue::new(v).ui(ui).changed(), ClientCrit::Pid(v) => DragValue::new(v).ui(ui).changed(), ClientCrit::IsXwayland => false, ClientCrit::Comm(v) => v.show(ui), ClientCrit::Exe(v) => v.show(ui), ClientCrit::Tag(v) => v.show(ui), } } fn to_crit(&self, state: &Rc) -> Option>> { let m = &state.cl_matcher_manager; let res = match self { ClientCrit::SandboxEngine(v) => m.sandbox_engine(v.to_crit()?), ClientCrit::SandboxAppId(v) => m.sandbox_app_id(v.to_crit()?), ClientCrit::SandboxInstanceId(v) => m.sandbox_instance_id(v.to_crit()?), ClientCrit::Sandboxed => m.sandboxed(), ClientCrit::Uid(v) => m.uid(*v), ClientCrit::Pid(v) => m.pid(*v), ClientCrit::IsXwayland => m.is_xwayland(), ClientCrit::Comm(v) => m.comm(v.to_crit()?), ClientCrit::Exe(v) => m.exe(v.to_crit()?), ClientCrit::Tag(v) => m.tag(v.to_crit()?), }; Some(res) } fn not( state: &State, upstream: &Rc>, ) -> Rc> { state.cl_matcher_manager.not(upstream) } fn list( state: &State, upstream: &[Rc>], all: bool, ) -> Rc> { state.cl_matcher_manager.list(upstream, all) } fn exactly( state: &State, n: usize, upstream: &[Rc>], ) -> Rc> { state.cl_matcher_manager.exactly(upstream, n) } } pub struct ClientsPane { state: Rc, filter: bool, criterion: CcCriterion, matched: Rc, leaf: Option>>>, } struct Matched { slf: Weak, clients: CopyHashMap, } impl Matched { fn request_frame(&self) { if let Some(slf) = self.slf.upgrade() { slf.window.request_redraw(); } } } impl ControlCenterInner { pub fn create_clients_pane(self: &Rc) -> ClientsPane { let mut pane = ClientsPane { state: self.state.clone(), filter: false, criterion: Default::default(), matched: Rc::new(Matched { slf: Rc::downgrade(self), clients: Default::default(), }), leaf: Default::default(), }; pane.update_matcher(); pane } } impl ClientsPane { pub fn title(&self, res: &mut String) { res.push_str("Clients"); } pub fn show(&mut self, behavior: &mut CcBehavior<'_>, ui: &mut Ui) { if ui.checkbox(&mut self.filter, "Filter").changed() && !self.filter { self.criterion = Default::default(); self.update_matcher(); } let mut clear_clients = false; if self.filter && self.criterion.show(ui) { clear_clients = self.update_matcher(); } ui.separator(); let mut clients: Vec<_> = self.matched.clients.lock().keys().copied().collect(); clients.sort(); for id in clients { let Ok(client) = self.state.clients.get(id) else { continue; }; show_client_collapsible(behavior, ui, &client); } if clear_clients { self.matched.clients.clear(); } } fn update_matcher(&mut self) -> bool { let mut clear_clients = false; let state = &self.state; if let Some(new) = self.criterion.to_crit(state) { clear_clients = true; let matched = self.matched.clone(); let leaf = state.cl_matcher_manager.leaf(&new, move |data| { matched.clients.set(data, ()); matched.request_frame(); Box::new({ let matched = matched.clone(); move || { matched.clients.remove(&data); matched.request_frame(); } }) }); state.cl_matcher_manager.rematch_all(state); self.leaf = Some(leaf); } clear_clients } } pub struct ClientPane { client: Rc, } impl ControlCenterInner { pub fn create_client_pane(self: &Rc, client: &Rc) -> ClientPane { ClientPane { client: client.clone(), } } } impl ClientPane { pub fn title(&self, res: &mut String) { res.push_str("Client "); res.push_str(&self.client.pid_info.comm); } pub fn show(&mut self, behavior: &mut CcBehavior<'_>, ui: &mut Ui) { show_client(behavior, ui, &self.client); } } pub fn show_client_collapsible(behavior: &mut CcBehavior, ui: &mut Ui, client: &Rc) { let mut layout_job = LayoutJob::default(); layout_job.append( "Client", 0.0, TextFormat { color: ui.style().visuals.widgets.inactive.text_color(), ..Default::default() }, ); layout_job.append( &client.id.to_string(), 10.0, TextFormat { color: ui.style().visuals.widgets.active.text_color(), ..Default::default() }, ); layout_job.append( &client.pid_info.comm, 10.0, TextFormat { color: ui.style().visuals.widgets.inactive.text_color(), ..Default::default() }, ); CollapsingHeader::new(layout_job) .id_salt(("client", client.id)) .show(ui, |ui| { if icon_label(ICON_OPEN_IN_NEW) .sense(Sense::CLICK) .ui(ui) .clicked() { behavior.open = Some(PaneType::Client(behavior.cc.create_client_pane(client))); } show_client(behavior, ui, client) }); } pub fn show_client(behavior: &mut CcBehavior<'_>, ui: &mut Ui, client: &Client) { grid(ui, ("client", client.id), |ui| { label(ui, "ID", client.id.to_string()); label(ui, "PID", client.pid_info.pid.to_string()); label(ui, "UID", client.pid_info.uid.to_string()); label(ui, "comm", &client.pid_info.comm); label(ui, "exe", &client.pid_info.exe); if client.acceptor.sandboxed { read_only_bool(ui, "Sandboxed", true); } if client.acceptor.secure { read_only_bool(ui, "Secure", true); } if client.is_xwayland { read_only_bool(ui, "Xwayland", true); } if let Some(v) = &client.acceptor.sandbox_engine { label(ui, "Sandbox Engine", v); } if let Some(v) = &client.acceptor.app_id { label(ui, "App ID", v); } if let Some(v) = &client.acceptor.instance_id { label(ui, "Instance ID", v); } if let Some(v) = &client.acceptor.tag { label(ui, "Tag", v); } }); if ui.button("Kill").clicked() { client.state.clients.kill(client.id); } ui.collapsing("Capabilities", |ui| { ui.add_enabled_ui(false, |ui| { for (k, v) in client.effective_caps.get().to_map() { if v { ui.checkbox(&mut true, k.text()); } } }); }); ui.collapsing("Windows", |ui| { let matcher = ui.memory_mut(|m| { m.caches .cache::() .get(behavior.cc, client.id) .clone() }); let mut windows: Vec<_> = matcher.windows.lock().keys().copied().collect(); windows.sort(); for id in windows { let Some(window) = client.state.toplevels.get(&id).and_then(|v| v.upgrade()) else { continue; }; show_window_collapsible(behavior, ui, &window); } }); } #[derive(Default)] struct ClientWindowMatchersCache { generation: u64, matchers: AHashMap, } struct CachedWindowMatcher { generation: u64, _matcher: Rc>, matchers: Rc, } struct WindowMatchers { cc: Weak, windows: CopyHashMap, } impl ClientWindowMatchersCache { fn get(&mut self, cc: &Rc, id: ClientId) -> &Rc { let res = self.matchers.entry(id).or_insert_with(|| { let state = &cc.state; let node = state.cl_matcher_manager.id(id); let node = state.tl_matcher_manager.client(state, &node); let matchers = Rc::new(WindowMatchers { cc: Rc::downgrade(&cc), windows: Default::default(), }); let matchers2 = matchers.clone(); let matcher = state.tl_matcher_manager.leaf(&node, move |id| { matchers2.windows.set(id, ()); if let Some(cc) = matchers2.cc.upgrade() { cc.window.request_redraw(); } let matchers2 = matchers2.clone(); Box::new(move || { matchers2.windows.remove(&id); if let Some(cc) = matchers2.cc.upgrade() { cc.window.request_redraw(); } }) }); let res = CachedWindowMatcher { generation: 0, _matcher: matcher, matchers, }; state.cl_matcher_manager.rematch_all(state); state.tl_matcher_manager.rematch_all(state); res }); res.generation = self.generation; &res.matchers } } unsafe impl Sync for ClientWindowMatchersCache {} unsafe impl Send for ClientWindowMatchersCache {} impl CacheTrait for ClientWindowMatchersCache { fn update(&mut self) { self.matchers.retain(|_, m| m.generation == self.generation); self.generation += 1; } fn len(&self) -> usize { self.matchers.len() } fn as_any_mut(&mut self) -> &mut dyn Any { self } }