All checks were successful
Build AppImage / build (push) Successful in 8m53s
- 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>
168 lines
5.5 KiB
Rust
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)
|
|
}
|