agentd 0.1.0

Agent daemon for secure capability execution with pluggable isolation backends
Documentation
#[cfg(feature = "grpc")]
use std::collections::HashMap;
#[cfg(feature = "grpc")]
use tracing::warn;

#[cfg(feature = "grpc")]
use crate::desktop::types::{UiBounds, UiWindowRecord};

#[cfg(feature = "grpc")]
use super::{command_exists, run_host_command};

#[cfg(feature = "grpc")]
#[derive(Debug, serde::Deserialize)]
struct HyprWorkspace {
    id: i32,
    name: String,
}

#[cfg(feature = "grpc")]
#[derive(Debug, serde::Deserialize)]
struct HyprClient {
    address: String,
    mapped: bool,
    hidden: bool,
    at: Vec<i32>,
    size: Vec<i32>,
    class: Option<String>,
    title: Option<String>,
    workspace: Option<HyprWorkspace>,
}

#[cfg(feature = "grpc")]
fn normalize_window_text(value: Option<String>) -> Option<String> {
    value.and_then(|raw| {
        let trimmed = raw.trim();
        if trimmed.is_empty() {
            None
        } else {
            Some(trimmed.to_string())
        }
    })
}

#[cfg(feature = "grpc")]
fn parse_bounds(client: &HyprClient) -> Option<UiBounds> {
    if client.at.len() < 2 || client.size.len() < 2 {
        return None;
    }
    let width = u32::try_from(client.size[0]).ok()?;
    let height = u32::try_from(client.size[1]).ok()?;
    if width == 0 || height == 0 {
        return None;
    }
    Some(UiBounds {
        x: client.at[0],
        y: client.at[1],
        width,
        height,
    })
}

#[cfg(feature = "grpc")]
pub(super) async fn collect_windows(
    env: &HashMap<String, String>,
    timeout_ms: u64,
) -> Vec<UiWindowRecord> {
    if !command_exists("hyprctl") {
        return vec![];
    }
    if env
        .get("HYPRLAND_INSTANCE_SIGNATURE")
        .map(|v| v.trim().is_empty())
        .unwrap_or(true)
    {
        return vec![];
    }

    let args = vec!["-j".to_string(), "clients".to_string()];
    let (exit_code, stdout, stderr) =
        match run_host_command("hyprctl", &args, env, timeout_ms).await {
            Ok(output) => output,
            Err(err) => {
                warn!(error = %err, "Failed to query Hyprland clients");
                return vec![];
            }
        };

    if exit_code != 0 {
        warn!(
            exit_code,
            stderr = %stderr.trim(),
            "hyprctl clients query failed"
        );
        return vec![];
    }

    let clients: Vec<HyprClient> = match serde_json::from_str::<Vec<HyprClient>>(&stdout) {
        Ok(parsed) => parsed,
        Err(err) => {
            warn!(error = %err, "Failed to parse hyprctl clients JSON");
            return vec![];
        }
    };

    clients
        .into_iter()
        .filter(|client| client.mapped && !client.hidden)
        .filter_map(|client| {
            let bounds = parse_bounds(&client)?;
            let workspace = client
                .workspace
                .as_ref()
                .map(|ws| format!("{} ({})", ws.name, ws.id));
            let class_name = normalize_window_text(client.class.clone());
            let title = normalize_window_text(client.title.clone());
            Some(UiWindowRecord {
                window_id: format!("hyprland|/client/{}", client.address),
                destination: "hyprland".to_string(),
                object_path: format!("/client/{}", client.address),
                title,
                role: Some("window".to_string()),
                bounds: Some(bounds),
                source: Some("hyprland".to_string()),
                inspectable: Some(false),
                app_id: class_name.clone(),
                class_name,
                workspace,
            })
        })
        .collect()
}