claude-desktop/src-tauri/src/clipboard.rs
Eddy 87ba8f7bdf
All checks were successful
Build AppImage / build (push) Successful in 8m53s
feat: Global Hotkey (Super+C) + Clipboard-Watch + Desktop-Integration [appimage]
- Global Hotkey: Super+C öffnet Claude-Fenster und fokussiert Input von überall
- Clipboard-Watch: Erkennt Code/URLs/Fehler/Pfade in Zwischenablage, zeigt Vorschlag
- tauri-plugin-global-shortcut + tauri-plugin-dialog integriert
- focus-chat-input Event für Hotkey → Input-Fokus
- clipboard-changed Event für Watch → Chat-Vorschlag

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-21 11:50:56 +02:00

168 lines
5.5 KiB
Rust

// 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<Mutex<ClipboardWatcher>>;
/// 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 ", "=>", "->", "<?php", "#!/",
"{", "}", "();", "= {", "= ["];
let code_score: usize = code_indicators.iter()
.filter(|ind| trimmed.contains(*ind))
.count();
if code_score >= 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::<ClipboardState>();
{
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::<ClipboardState>().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::<ClipboardState>();
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<bool, String> {
let state = app.state::<ClipboardState>();
let watcher = state.lock().unwrap();
Ok(watcher.enabled)
}