jay-compositor 1.15.0

The Jay compositor
use crate::cli::GlobalArgs;
use crate::cli::MatchSource;
use crate::cli::json::JsonClient;
use crate::cli::json::jsonl;
use crate::tools::tool_client::Handle;
use crate::tools::tool_client::ToolClient;
use crate::tools::tool_client::with_tool_client;
use crate::utils::bhash::BHashMap;
use crate::utils::errorfmt::ErrorFmt;
use crate::wire::JayClientQueryId;
use crate::wire::jay_client_query;
use crate::wire::jay_compositor;
use clap::Args;
use clap::Subcommand;
use jay_toml_config::ClientMatch;
use jay_toml_config::parse_client_match;
use std::cell::RefCell;
use std::mem;
use std::rc::Rc;
use uapi::c;

#[derive(Args, Debug)]
pub struct ClientsArgs {
    #[clap(subcommand)]
    cmd: Option<ClientsCmd>,
}

#[derive(Subcommand, Debug)]
enum ClientsCmd {
    /// Show information about clients.
    Show(ShowArgs),
    /// Disconnect a client.
    Kill(KillArgs),
}

#[derive(Args, Debug)]
struct ShowArgs {
    #[clap(subcommand)]
    cmd: ShowCmd,
}

#[derive(Subcommand, Debug)]
enum ShowCmd {
    /// Show all clients.
    All,
    /// Show a client with a given ID.
    Id(ShowIdArgs),
    /// Interactively select a window and show information about its client.
    SelectWindow,
    /// Select clients via a toml client match.
    Match(MatchSource),
}

#[derive(Args, Debug)]
struct ShowIdArgs {
    /// The ID of the client.
    id: u64,
}

#[derive(Args, Debug)]
struct KillArgs {
    #[clap(subcommand)]
    cmd: KillCmd,
}

#[derive(Subcommand, Debug)]
enum KillCmd {
    /// Kill the client with a given ID.
    Id(KillIdArgs),
    /// Interactively select a window and kill its client.
    SelectWindow,
    /// Kill clients via a toml client match.
    Match(MatchSource),
}

#[derive(Args, Debug)]
struct KillIdArgs {
    /// The ID of the client.
    id: u64,
}

pub fn main(global: GlobalArgs, clients_args: ClientsArgs) {
    with_tool_client(async move |tc| {
        let clients = Rc::new(Clients { tc: tc.clone() });
        clients.run(&global, clients_args).await;
    });
}

struct Clients {
    tc: Rc<ToolClient>,
}

impl Clients {
    async fn run(&self, global: &GlobalArgs, args: ClientsArgs) {
        let tc = &self.tc;
        let comp = tc.jay_compositor().await;
        let cmd = args
            .cmd
            .unwrap_or(ClientsCmd::Show(ShowArgs { cmd: ShowCmd::All }));
        match cmd {
            ClientsCmd::Show(a) => {
                let id = tc.id();
                tc.send(jay_compositor::CreateClientQuery { self_id: comp, id });
                match a.cmd {
                    ShowCmd::All => {
                        tc.send(jay_client_query::AddAll { self_id: id });
                    }
                    ShowCmd::Id(a) => {
                        tc.send(jay_client_query::AddId {
                            self_id: id,
                            id: a.id,
                        });
                    }
                    ShowCmd::SelectWindow => {
                        let client_id = tc.select_toplevel_client().await;
                        if client_id == 0 {
                            fatal!("Did not select a window");
                        }
                        tc.send(jay_client_query::AddId {
                            self_id: id,
                            id: client_id,
                        });
                    }
                    ShowCmd::Match(a) => {
                        let client_match = a.parse_client_match(tc, false);
                        let client_match = tc.create_client_match(comp, &client_match);
                        tc.send(jay_client_query::AddMatch {
                            self_id: id,
                            id: client_match,
                        });
                    }
                }
                tc.send(jay_client_query::Execute { self_id: id });
                let clients = handle_client_query(tc, id).await;
                let mut clients = clients.values().collect::<Vec<_>>();
                clients.sort_by_key(|c| c.id);
                if global.json {
                    for client in clients {
                        let client = make_json_client(client);
                        jsonl(&client);
                    }
                } else {
                    let mut prefix = "    ".to_string();
                    let mut printer = ClientPrinter {
                        prefix: &mut prefix,
                    };
                    for client in clients {
                        println!("- client:");
                        printer.print_client(client);
                    }
                }
            }
            ClientsCmd::Kill(a) => match a.cmd {
                KillCmd::Id(id) => {
                    tc.send(jay_compositor::KillClient {
                        self_id: comp,
                        id: id.id,
                    });
                }
                KillCmd::SelectWindow => {
                    let client_id = tc.select_toplevel_client().await;
                    if client_id == 0 {
                        fatal!("Did not select a window");
                    }
                    tc.send(jay_compositor::KillClient {
                        self_id: comp,
                        id: client_id,
                    });
                }
                KillCmd::Match(a) => {
                    let client_match = a.parse_client_match(tc, true);
                    let client_match = tc.create_client_match(comp, &client_match);
                    tc.send(jay_compositor::KillMatchedClients {
                        self_id: comp,
                        id: client_match,
                    });
                }
            },
        }
        tc.round_trip().await;
    }
}

#[derive(Default)]
pub struct Client {
    id: u64,
    sandboxed: bool,
    sandbox_engine: Option<String>,
    sandbox_app_id: Option<String>,
    sandbox_instance_id: Option<String>,
    uid: Option<c::uid_t>,
    pid: Option<c::pid_t>,
    is_xwayland: bool,
    comm: Option<String>,
    exe: Option<String>,
    tag: Option<String>,
}

pub async fn handle_client_query(
    tl: &Rc<ToolClient>,
    id: JayClientQueryId,
) -> BHashMap<u64, Client> {
    use jay_client_query::*;
    let c = Rc::new(RefCell::new(Vec::<Client>::new()));
    macro_rules! last {
        ($c:ident) => {
            $c.borrow_mut().last_mut().unwrap()
        };
    }
    Start::handle(tl, id, c.clone(), |c, event| {
        c.borrow_mut().push(Client::default());
        last!(c).id = event.id;
    });
    Sandboxed::handle(tl, id, c.clone(), |c, _event| {
        last!(c).sandboxed = true;
    });
    SandboxEngine::handle(tl, id, c.clone(), |c, event| {
        last!(c).sandbox_engine = Some(event.engine.to_string());
    });
    SandboxAppId::handle(tl, id, c.clone(), |c, event| {
        last!(c).sandbox_app_id = Some(event.app_id.to_string());
    });
    SandboxInstanceId::handle(tl, id, c.clone(), |c, event| {
        last!(c).sandbox_instance_id = Some(event.instance_id.to_string());
    });
    Uid::handle(tl, id, c.clone(), |c, event| {
        last!(c).uid = Some(event.uid);
    });
    Pid::handle(tl, id, c.clone(), |c, event| {
        last!(c).pid = Some(event.pid);
    });
    IsXwayland::handle(tl, id, c.clone(), |c, _event| {
        last!(c).is_xwayland = true;
    });
    Comm::handle(tl, id, c.clone(), |c, event| {
        last!(c).comm = Some(event.comm.to_string());
    });
    Exe::handle(tl, id, c.clone(), |c, event| {
        last!(c).exe = Some(event.exe.to_string());
    });
    Tag::handle(tl, id, c.clone(), |c, event| {
        last!(c).tag = Some(event.tag.to_string());
    });
    tl.round_trip().await;
    mem::take(&mut *c.borrow_mut())
        .into_iter()
        .map(|c| (c.id, c))
        .collect()
}

pub struct ClientPrinter<'a> {
    pub prefix: &'a mut String,
}

impl ClientPrinter<'_> {
    pub fn print_client(&mut self, c: &Client) {
        let p = &self.prefix;
        macro_rules! opt {
            ($field:ident, $pretty:expr) => {
                if let Some(v) = &c.$field {
                    println!("{p}{}: {}", $pretty, v);
                }
            };
        }
        macro_rules! bol {
            ($field:ident, $pretty:expr) => {
                if c.$field {
                    println!("{p}{}", $pretty);
                }
            };
        }
        println!("{p}id: {}", c.id);
        bol!(sandboxed, "sandboxed");
        opt!(sandbox_engine, "sandbox engine");
        opt!(sandbox_app_id, "sandbox app id");
        opt!(sandbox_instance_id, "sandbox instance id");
        opt!(uid, "uid");
        opt!(pid, "pid");
        bol!(is_xwayland, "xwayland");
        opt!(comm, "comm");
        opt!(exe, "exe");
        opt!(tag, "tag");
    }
}

pub fn make_json_client(client: &Client) -> JsonClient<'_> {
    JsonClient {
        client_id: client.id,
        sandboxed: client.sandboxed,
        sandbox_engine: client.sandbox_engine.as_deref(),
        sandbox_app_id: client.sandbox_app_id.as_deref(),
        sandbox_instance_id: client.sandbox_instance_id.as_deref(),
        uid: client.uid,
        pid: client.pid,
        is_xwayland: client.is_xwayland,
        comm: client.comm.as_deref(),
        exe: client.exe.as_deref(),
        tag: client.tag.as_deref(),
    }
}

impl MatchSource {
    pub fn parse_client_match(&self, tc: &ToolClient, strict: bool) -> ClientMatch {
        let mut source = vec![];
        let path = self.read(&mut source);
        let warnings = tc.logger.warnings();
        match parse_client_match(&source) {
            Ok(m) => {
                if strict && tc.logger.warnings() > warnings {
                    fatal!("Aborting due to warnings");
                }
                m
            }
            Err(e) => fatal!("Could not parse {path}: {}", ErrorFmt(&*e)),
        }
    }
}