diff --git a/src/control_center.rs b/src/control_center.rs index a78884f8..661afba0 100644 --- a/src/control_center.rs +++ b/src/control_center.rs @@ -1,9 +1,15 @@ use { crate::{ control_center::{ - cc_color_management::ColorManagementPane, cc_compositor::CompositorPane, - cc_gpus::GpusPane, cc_idle::IdlePane, cc_input::InputPane, - cc_look_and_feel::LookAndFeelPane, cc_outputs::OutputsPane, cc_xwayland::XwaylandPane, + cc_clients::{ClientPane, ClientsPane}, + cc_color_management::ColorManagementPane, + cc_compositor::CompositorPane, + cc_gpus::GpusPane, + cc_idle::IdlePane, + cc_input::InputPane, + cc_look_and_feel::LookAndFeelPane, + cc_outputs::OutputsPane, + cc_xwayland::XwaylandPane, }, egui_adapter::egui_platform::{ EggError, EggWindow, EggWindowOwner, @@ -34,8 +40,10 @@ use { thiserror::Error, }; +mod cc_clients; mod cc_color_management; mod cc_compositor; +mod cc_criterion; mod cc_gpus; mod cc_idle; mod cc_input; @@ -131,10 +139,11 @@ enum PaneType { GPUs(GpusPane), Input(InputPane), LookAndFeel(LookAndFeelPane), + Clients(ClientsPane), + Client(ClientPane), } struct CcBehavior<'a> { - #[expect(dead_code)] cc: &'a Rc, close: Option, open: Option, @@ -157,6 +166,8 @@ impl Pane { PaneType::GPUs(v) => v.title(res), PaneType::Input(v) => v.title(res), PaneType::LookAndFeel(v) => v.title(res), + PaneType::Clients(v) => v.title(res), + PaneType::Client(v) => v.title(res), } } @@ -170,6 +181,8 @@ impl Pane { PaneType::GPUs(p) => p.show(ui), PaneType::Input(p) => p.show(&mut self.ps, ui), PaneType::LookAndFeel(p) => p.show(ui), + PaneType::Clients(p) => p.show(behavior, ui), + PaneType::Client(p) => p.show(behavior, ui), } } } @@ -185,6 +198,8 @@ impl PaneType { PaneType::GPUs(_) => CCI_GPUS, PaneType::Input(_) => CCI_INPUT, PaneType::LookAndFeel(_) => CCI_LOOK_AND_FEEL, + PaneType::Clients(_) => ControlCenterInterest::none(), + PaneType::Client(_) => ControlCenterInterest::none(), } } } diff --git a/src/control_center/cc_clients.rs b/src/control_center/cc_clients.rs new file mode 100644 index 00000000..8ffa73af --- /dev/null +++ b/src/control_center/cc_clients.rs @@ -0,0 +1,362 @@ +use { + crate::{ + client::{Client, ClientId}, + control_center::{ + CcBehavior, ControlCenterInner, PaneType, + cc_criterion::{CcCriterion, CritImpl, CritRegex}, + grid, icon_label, label, read_only_bool, + }, + criteria::{CritMgrExt, CritUpstreamNode, crit_leaf::CritLeafMatcher}, + egui_adapter::egui_platform::icons::ICON_OPEN_IN_NEW, + state::State, + utils::{copyhashmap::CopyHashMap, static_text::StaticText}, + }, + egui::{CollapsingHeader, DragValue, Sense, TextFormat, Ui, Widget, text::LayoutJob}, + linearize::Linearize, + std::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()); + } + } + }); + }); +} diff --git a/src/control_center/cc_criterion.rs b/src/control_center/cc_criterion.rs new file mode 100644 index 00000000..6c93acd7 --- /dev/null +++ b/src/control_center/cc_criterion.rs @@ -0,0 +1,254 @@ +use { + crate::{ + criteria::{CritLiteralOrRegex, CritUpstreamNode}, + egui_adapter::egui_platform::icons::ICON_CLOSE, + state::State, + utils::{numcell::NumCell, static_text::StaticText}, + }, + ahash::AHashSet, + egui::{ComboBox, DragValue, Ui, UiBuilder, Widget}, + isnt::std_1::collections::IsntHashSetExt, + linearize::{Linearize, LinearizeExt}, + regex::Regex, + std::rc::Rc, +}; + +pub enum CcCriterion { + Not(Box), + List(Vec, bool), + Exactly(usize, Vec), + T(T), +} + +impl Default for CcCriterion +where + T: Default, +{ + fn default() -> Self { + Self::T(T::default()) + } +} + +#[derive(Copy, Clone, Eq, PartialEq, Linearize)] +enum CompoundCritTy { + Not, + All, + Any, + Exactly, +} + +#[derive(Copy, Clone, Eq, PartialEq)] +enum CritTy { + Compound(CompoundCritTy), + T(T), +} + +impl StaticText for CompoundCritTy { + fn text(&self) -> &'static str { + match self { + Self::Not => "Not", + Self::All => "All", + Self::Any => "Any", + Self::Exactly => "Exactly", + } + } +} + +impl StaticText for CritTy +where + T: StaticText, +{ + fn text(&self) -> &'static str { + match self { + Self::Compound(t) => t.text(), + Self::T(t) => t.text(), + } + } +} + +pub trait CritImpl: Default { + type Type: Copy + Eq + PartialEq + StaticText + Linearize; + type Target; + + fn ty(&self) -> Self::Type; + fn from_ty(ty: Self::Type) -> Self; + #[must_use] + fn show(&mut self, ui: &mut Ui) -> bool; + + fn to_crit(&self, state: &Rc) -> Option>>; + fn not( + state: &State, + upstream: &Rc>, + ) -> Rc>; + fn list( + state: &State, + upstream: &[Rc>], + all: bool, + ) -> Rc>; + fn exactly( + state: &State, + n: usize, + upstream: &[Rc>], + ) -> Rc>; +} + +impl CcCriterion +where + T: CritImpl, +{ + #[must_use] + pub fn show(&mut self, ui: &mut Ui) -> bool { + let mut changed = false; + ui.vertical(|ui| { + ui.horizontal(|ui| { + let mut v = self.ty(); + let old = v; + ComboBox::from_id_salt("ty") + .selected_text(v.text()) + .show_ui(ui, |ui| { + for s in CompoundCritTy::variants() { + ui.selectable_value(&mut v, CritTy::Compound(s), s.text()); + } + for s in T::Type::variants() { + ui.selectable_value(&mut v, CritTy::T(s), s.text()); + } + }); + if old != v { + *self = match v { + CritTy::Compound(CompoundCritTy::Not) => { + CcCriterion::Not(Default::default()) + } + CritTy::Compound(CompoundCritTy::All) => { + CcCriterion::List(Default::default(), true) + } + CritTy::Compound(CompoundCritTy::Any) => { + CcCriterion::List(Default::default(), false) + } + CritTy::Compound(CompoundCritTy::Exactly) => { + CcCriterion::Exactly(1, Default::default()) + } + CritTy::T(t) => CcCriterion::T(T::from_ty(t)), + }; + changed = true; + } + match self { + CcCriterion::Not(n) => changed |= n.show(ui), + CcCriterion::List(_, _) => {} + CcCriterion::Exactly(n, _) => { + changed |= DragValue::new(n).ui(ui).changed(); + } + CcCriterion::T(t) => changed |= t.show(ui), + } + }); + match self { + CcCriterion::Not(_) => {} + CcCriterion::List(v, _) | CcCriterion::Exactly(_, v) => { + ui.indent("compound", |ui| { + let mut to_remove = AHashSet::new(); + for (idx, v) in v.iter_mut().enumerate() { + ui.horizontal(|ui| { + if ui.button(ICON_CLOSE).clicked() { + changed = true; + to_remove.insert(idx); + } + ui.scope_builder(UiBuilder::new().id_salt(idx), |ui| { + changed |= v.show(ui); + }); + }); + } + let i = NumCell::new(0); + v.retain(|_| to_remove.not_contains(&i.fetch_add(1))); + if ui.button("Add").clicked() { + v.push(CcCriterion::default()); + changed = true; + } + }); + } + CcCriterion::T(_) => {} + } + }); + changed + } + + fn ty(&self) -> CritTy { + match self { + CcCriterion::Not(_) => CritTy::Compound(CompoundCritTy::Not), + CcCriterion::List(_, true) => CritTy::Compound(CompoundCritTy::All), + CcCriterion::List(_, false) => CritTy::Compound(CompoundCritTy::Any), + CcCriterion::Exactly(_, _) => CritTy::Compound(CompoundCritTy::Exactly), + CcCriterion::T(t) => CritTy::T(t.ty()), + } + } + + pub fn to_crit(&self, state: &Rc) -> Option>> { + match self { + CcCriterion::Not(t) => Some(T::not(state, &t.to_crit(state)?)), + CcCriterion::List(v, all) => { + let mut upstream = Vec::with_capacity(v.len()); + for v in v { + upstream.push(v.to_crit(state)?); + } + Some(T::list(state, &upstream, *all)) + } + CcCriterion::Exactly(n, v) => { + let mut upstream = Vec::with_capacity(v.len()); + for v in v { + upstream.push(v.to_crit(state)?); + } + Some(T::exactly(state, *n, &upstream)) + } + CcCriterion::T(t) => t.to_crit(state), + } + } + + #[expect(dead_code)] + pub fn any(&self, mut any: impl FnMut(&T) -> bool) -> bool { + self.any_(&mut any) + } + + fn any_(&self, any: &mut impl FnMut(&T) -> bool) -> bool { + match self { + CcCriterion::Not(v) => v.any_(any), + CcCriterion::List(v, _) => v.iter().any(|v| v.any_(any)), + CcCriterion::Exactly(_, v) => v.iter().any(|v| v.any_(any)), + CcCriterion::T(t) => any(t), + } + } +} + +pub struct CritRegex { + pub text: String, + pub regex: Option>, +} + +impl Default for CritRegex { + fn default() -> Self { + Self { + text: Default::default(), + regex: Some(Some(Regex::new("").unwrap())), + } + } +} + +impl CritRegex { + pub fn show(&mut self, ui: &mut Ui) -> bool { + let mut is_regex = self.regex.is_some(); + let mut changed = false; + changed |= ui.text_edit_singleline(&mut self.text).changed(); + changed |= ui.checkbox(&mut is_regex, "Regex").changed(); + if changed { + self.regex = is_regex.then(|| Regex::new(&self.text).ok()); + } + if let Some(None) = self.regex { + ui.label("Error: Invalid regex"); + } + changed + } + + pub fn to_crit(&self) -> Option { + match &self.regex { + None => Some(CritLiteralOrRegex::Literal(self.text.clone())), + Some(v) => Some(CritLiteralOrRegex::Regex(v.clone()?)), + } + } +} diff --git a/src/control_center/cc_sidebar.rs b/src/control_center/cc_sidebar.rs index 6215bb2a..61b5194c 100644 --- a/src/control_center/cc_sidebar.rs +++ b/src/control_center/cc_sidebar.rs @@ -16,6 +16,7 @@ enum PaneName { GPUs, Input, LookAndFeel, + Clients, } impl PaneName { @@ -29,6 +30,7 @@ impl PaneName { PaneName::GPUs => "GPUs", PaneName::Input => "Input", PaneName::LookAndFeel => "Look and Feel", + PaneName::Clients => "Clients", } } } @@ -71,6 +73,7 @@ impl ControlCenterInner { PaneName::LookAndFeel => { PaneType::LookAndFeel(self.create_look_and_feel_pane()) } + PaneName::Clients => PaneType::Clients(self.create_clients_pane()), }; self.open(tree, ty); ui.ctx().request_repaint(); diff --git a/src/control_center/cc_xwayland.rs b/src/control_center/cc_xwayland.rs index 656f059b..2adedeb8 100644 --- a/src/control_center/cc_xwayland.rs +++ b/src/control_center/cc_xwayland.rs @@ -2,7 +2,8 @@ use { crate::{ compositor::DISPLAY, control_center::{ - CcBehavior, ControlCenterInner, bool, combo_box_ui, grid, label, read_only_bool, tip, + CcBehavior, ControlCenterInner, bool, cc_clients::show_client_collapsible, + combo_box_ui, grid, label, read_only_bool, tip, }, state::State, utils::{errorfmt::ErrorFmt, oserror::OsError, static_text::StaticText}, @@ -45,7 +46,7 @@ impl XwaylandPane { res.push_str("Xwayland"); } - pub fn show(&mut self, _behavior: &mut CcBehavior<'_>, ui: &mut Ui) { + pub fn show(&mut self, behavior: &mut CcBehavior<'_>, ui: &mut Ui) { let s = &self.state; grid(ui, "settings", |ui| { bool(ui, "Enabled", s.xwayland.enabled.get(), |b| { @@ -83,5 +84,8 @@ impl XwaylandPane { { log::error!("Could not kill Xwayland: {}", ErrorFmt(OsError::from(e))); } + if let Some(client) = self.state.xwayland.client.get() { + show_client_collapsible(behavior, ui, &client); + } } } diff --git a/src/egui_adapter/egui_platform.rs b/src/egui_adapter/egui_platform.rs index 920ea679..d6d32744 100644 --- a/src/egui_adapter/egui_platform.rs +++ b/src/egui_adapter/egui_platform.rs @@ -114,7 +114,6 @@ pub mod icons { pub const ICON_CLOSE: &str = "\u{e5cd}"; pub const ICON_DRAG_INDICATOR: &str = "\u{e945}"; pub const ICON_INFO: &str = "\u{e88e}"; - #[expect(dead_code)] pub const ICON_OPEN_IN_NEW: &str = "\u{e89e}"; pub const ICON_PENDING: &str = "\u{ef64}"; pub const ICON_REMOVE: &str = "\u{e15b}";