// Claude Desktop — Programm-Steuerung // D-Bus: Linux-Apps via dbus-send/qdbus // Xvfb: Virtuelles Display fuer Computer-Use (Scaffold) // Playwright: Tool-Hinweise (eigentliche Steuerung laeuft ueber MCP-Server) use serde::{Deserialize, Serialize}; use std::process::Command; // ============ D-Bus ============ #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DbusCallResult { pub success: bool, pub stdout: String, pub stderr: String, pub exit_code: i32, } #[tauri::command] pub async fn dbus_call( service: String, path: String, method: String, args: Option>, session: Option, ) -> Result { let bus = if session.unwrap_or(true) { "--session" } else { "--system" }; let mut cmd = Command::new("dbus-send"); cmd.arg("--print-reply") .arg(bus) .arg(format!("--dest={}", service)) .arg(&path) .arg(&method); if let Some(args) = args { for a in args { cmd.arg(a); } } let output = cmd.output().map_err(|e| format!("dbus-send Fehler: {}", e))?; Ok(DbusCallResult { success: output.status.success(), stdout: String::from_utf8_lossy(&output.stdout).to_string(), stderr: String::from_utf8_lossy(&output.stderr).to_string(), exit_code: output.status.code().unwrap_or(-1), }) } #[tauri::command] pub async fn dbus_list_services(session: Option) -> Result, String> { let bus = if session.unwrap_or(true) { "--session" } else { "--system" }; let output = Command::new("dbus-send") .arg("--print-reply") .arg(bus) .arg("--dest=org.freedesktop.DBus") .arg("/org/freedesktop/DBus") .arg("org.freedesktop.DBus.ListNames") .output() .map_err(|e| format!("dbus-send ListNames: {}", e))?; if !output.status.success() { return Err(String::from_utf8_lossy(&output.stderr).to_string()); } let text = String::from_utf8_lossy(&output.stdout); let mut services: Vec = text .lines() .filter_map(|l| { let t = l.trim(); if t.starts_with("string \"") && t.ends_with('"') { Some(t[8..t.len() - 1].to_string()) } else { None } }) .filter(|s| !s.starts_with(':')) .collect(); services.sort(); services.dedup(); Ok(services) } // ============ Xvfb (Virtuelles Display) ============ #[derive(Debug, Clone, Serialize, Deserialize)] pub struct XvfbStatus { pub running: bool, pub display_num: u16, pub pid: Option, pub resolution: String, } static XVFB_STATE: std::sync::OnceLock> = std::sync::OnceLock::new(); fn xvfb_state() -> &'static std::sync::Mutex { XVFB_STATE.get_or_init(|| { std::sync::Mutex::new(XvfbStatus { running: false, display_num: 1, pid: None, resolution: "1920x1080x24".into(), }) }) } #[tauri::command] pub async fn xvfb_start(display_num: Option, resolution: Option) -> Result { let display = display_num.unwrap_or(1); let res = resolution.unwrap_or_else(|| "1920x1080x24".into()); // Pruefen ob Xvfb verfuegbar let check = Command::new("which").arg("Xvfb").output(); if check.map(|o| !o.status.success()).unwrap_or(true) { return Err("Xvfb nicht installiert. Auf NixOS: nixpkgs.xorg.xvfb".into()); } let child = Command::new("Xvfb") .arg(format!(":{}", display)) .arg("-screen") .arg("0") .arg(&res) .spawn() .map_err(|e| format!("Xvfb-Start fehlgeschlagen: {}", e))?; let status = XvfbStatus { running: true, display_num: display, pid: Some(child.id()), resolution: res, }; *xvfb_state().lock().unwrap() = status.clone(); Ok(status) } #[tauri::command] pub async fn xvfb_stop() -> Result { let mut state = xvfb_state().lock().unwrap(); if let Some(pid) = state.pid { let _ = Command::new("kill").arg(pid.to_string()).output(); } state.running = false; state.pid = None; Ok(state.clone()) } #[tauri::command] pub async fn xvfb_status() -> Result { Ok(xvfb_state().lock().unwrap().clone()) } #[tauri::command] pub async fn xvfb_screenshot(display_num: Option) -> Result { // Screenshot vom virtuellen Display — probiert mehrere Tools durch let display = display_num.unwrap_or(1); let display_env = format!(":{}", display); let tmp = std::env::temp_dir().join(format!("claude-xvfb-{}.png", uuid::Uuid::new_v4())); // Versuchte Kommandos in Reihenfolge let attempts: Vec<(&str, Vec)> = vec![ ("scrot", vec!["-q".into(), "80".into(), tmp.to_string_lossy().into_owned()]), ("import", vec!["-window".into(), "root".into(), tmp.to_string_lossy().into_owned()]), ("ffmpeg", vec![ "-f".into(), "x11grab".into(), "-i".into(), display_env.clone(), "-frames:v".into(), "1".into(), "-y".into(), tmp.to_string_lossy().into_owned(), ]), ]; let mut last_err = String::new(); for (cmd, args) in &attempts { // Tool vorhanden? if Command::new("which").arg(cmd).output().map(|o| !o.status.success()).unwrap_or(true) { last_err = format!("'{}' nicht installiert", cmd); continue; } let result = Command::new(cmd) .env("DISPLAY", &display_env) .args(args) .output(); match result { Ok(o) if o.status.success() && tmp.exists() => { let bytes = std::fs::read(&tmp).map_err(|e| e.to_string())?; let _ = std::fs::remove_file(&tmp); use base64::Engine; return Ok(base64::engine::general_purpose::STANDARD.encode(&bytes)); } Ok(o) => { last_err = format!("{} Exit {}: {}", cmd, o.status, String::from_utf8_lossy(&o.stderr)); } Err(e) => { last_err = format!("{} Fehler: {}", cmd, e); } } } Err(format!( "Screenshot fehlgeschlagen. Installiere eines: scrot / imagemagick / ffmpeg.\nLetzter Fehler: {}", last_err )) } // ============ Playwright-Infos ============ #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PlaywrightInfo { pub available: bool, pub hint: String, } #[tauri::command] pub async fn playwright_info() -> Result { // Playwright laeuft bei Eddy ueber MCP — hier nur Info fuer das UI Ok(PlaywrightInfo { available: true, hint: "Playwright-Steuerung laeuft ueber den MCP-Server. Nutze die Tools \ mcp__plugin_playwright_playwright__browser_navigate, \ _click, _snapshot etc. im Chat. Für Dolibarr-Automation \ empfohlen: Session-Login via MCP-Playwright speichern." .into(), }) }