// Claude Desktop — Clipboard-Watch // Überwacht die Zwischenablage und reagiert auf Änderungen // (Code-Snippets erklären, URLs zusammenfassen, etc.) use std::sync::{Arc, Mutex}; use tauri::{AppHandle, Manager, Emitter}; use serde::{Deserialize, Serialize}; /// Clipboard-Inhalt Typ #[derive(Debug, Clone, Serialize, Deserialize)] pub enum ClipboardContentType { Code, // Erkannter Code-Snippet Url, // URL/Link Error, // Fehlermeldung/Stacktrace Path, // Dateipfad Text, // Normaler Text } /// Clipboard-Event das ans Frontend gesendet wird #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ClipboardEvent { pub content: String, pub content_type: ClipboardContentType, pub suggestion: String, // Vorschlag was Claude damit tun könnte } /// Clipboard-State für Watch-Kontrolle pub struct ClipboardWatcher { pub enabled: bool, pub last_content: String, } impl Default for ClipboardWatcher { fn default() -> Self { Self { enabled: false, last_content: String::new(), } } } pub type ClipboardState = Arc>; /// Erkennt den Typ des Clipboard-Inhalts fn detect_content_type(text: &str) -> (ClipboardContentType, String) { let trimmed = text.trim(); // URL erkennen if trimmed.starts_with("http://") || trimmed.starts_with("https://") { return (ClipboardContentType::Url, "URL zusammenfassen?".to_string()); } // Dateipfad erkennen if trimmed.starts_with('/') && !trimmed.contains('\n') && trimmed.len() < 500 { return (ClipboardContentType::Path, "Datei analysieren?".to_string()); } // Fehlermeldung/Stacktrace erkennen let error_indicators = ["error", "Error", "ERROR", "exception", "Exception", "traceback", "Traceback", "panic", "PANIC", "at line", "stack trace"]; if error_indicators.iter().any(|e| trimmed.contains(e)) { return (ClipboardContentType::Error, "Fehler analysieren?".to_string()); } // Code erkennen (einfache Heuristik) let code_indicators = ["fn ", "function ", "def ", "class ", "import ", "const ", "let ", "var ", "pub ", "async ", "=>", "->", "= 2 || (trimmed.lines().count() > 3 && trimmed.contains('{')) { return (ClipboardContentType::Code, "Code erklären?".to_string()); } (ClipboardContentType::Text, String::new()) } /// Startet den Clipboard-Watcher (Polling alle 2s) #[tauri::command] pub async fn clipboard_watch_start(app: AppHandle) -> Result<(), String> { let state = app.state::(); { let mut watcher = state.lock().unwrap(); if watcher.enabled { return Ok(()); // Schon aktiv } watcher.enabled = true; } println!("📋 Clipboard-Watch gestartet"); let app_handle = app.clone(); let watch_state = app.state::().inner().clone(); tauri::async_runtime::spawn(async move { loop { tokio::time::sleep(std::time::Duration::from_secs(2)).await; let enabled = { let watcher = watch_state.lock().unwrap(); watcher.enabled }; if !enabled { break; } // Clipboard via xclip lesen (Linux) let output = tokio::process::Command::new("xclip") .args(["-selection", "clipboard", "-o"]) .output() .await; if let Ok(out) = output { if out.status.success() { let content = String::from_utf8_lossy(&out.stdout).to_string(); let trimmed = content.trim().to_string(); if trimmed.is_empty() || trimmed.len() > 10000 { continue; } let changed = { let mut watcher = watch_state.lock().unwrap(); if watcher.last_content == trimmed { false } else { watcher.last_content = trimmed.clone(); true } }; if changed { let (content_type, suggestion) = detect_content_type(&trimmed); // Nur bei interessanten Inhalten ein Event senden if !suggestion.is_empty() { let event = ClipboardEvent { content: trimmed, content_type, suggestion, }; let _ = app_handle.emit("clipboard-changed", &event); } } } } } println!("📋 Clipboard-Watch beendet"); }); Ok(()) } /// Stoppt den Clipboard-Watcher #[tauri::command] pub async fn clipboard_watch_stop(app: AppHandle) -> Result<(), String> { let state = app.state::(); let mut watcher = state.lock().unwrap(); watcher.enabled = false; println!("📋 Clipboard-Watch gestoppt"); Ok(()) } /// Gibt den aktuellen Clipboard-Status zurück #[tauri::command] pub async fn clipboard_watch_status(app: AppHandle) -> Result { let state = app.state::(); let watcher = state.lock().unwrap(); Ok(watcher.enabled) }