1
0
Fork 0
forked from wry/wry

control-center: add clients pane

This commit is contained in:
Julian Orth 2026-02-27 14:56:32 +01:00
parent 78d59927ee
commit aefd1fbbdb
6 changed files with 644 additions and 7 deletions

View file

@ -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<Client>;
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<State>) -> Option<Rc<dyn CritUpstreamNode<Self::Target>>> {
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<dyn CritUpstreamNode<Self::Target>>,
) -> Rc<dyn CritUpstreamNode<Self::Target>> {
state.cl_matcher_manager.not(upstream)
}
fn list(
state: &State,
upstream: &[Rc<dyn CritUpstreamNode<Self::Target>>],
all: bool,
) -> Rc<dyn CritUpstreamNode<Self::Target>> {
state.cl_matcher_manager.list(upstream, all)
}
fn exactly(
state: &State,
n: usize,
upstream: &[Rc<dyn CritUpstreamNode<Self::Target>>],
) -> Rc<dyn CritUpstreamNode<Self::Target>> {
state.cl_matcher_manager.exactly(upstream, n)
}
}
pub struct ClientsPane {
state: Rc<State>,
filter: bool,
criterion: CcCriterion<ClientCrit>,
matched: Rc<Matched>,
leaf: Option<Rc<CritLeafMatcher<Rc<Client>>>>,
}
struct Matched {
slf: Weak<ControlCenterInner>,
clients: CopyHashMap<ClientId, ()>,
}
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<Self>) -> 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<Client>,
}
impl ControlCenterInner {
pub fn create_client_pane(self: &Rc<Self>, client: &Rc<Client>) -> 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<Client>) {
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());
}
}
});
});
}