feat: Global Hotkey (Super+C) + Clipboard-Watch + Desktop-Integration [appimage]
All checks were successful
Build AppImage / build (push) Successful in 8m53s
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>
This commit is contained in:
parent
6efbd5de5f
commit
87ba8f7bdf
8 changed files with 288 additions and 8 deletions
|
|
@ -13,6 +13,9 @@ Format angelehnt an [Keep a Changelog](https://keepachangelog.com/de/1.0.0/).
|
|||
- **File-Drop auf Chat**: Dateien per Drag & Drop auf den Chat ziehen — Text-Dateien als Code-Block, Bilder als Base64, Spracherkennung, 500KB-Limit (`ChatPanel.svelte`)
|
||||
- **Persistent Memory**: Auto-Load Memory-Einträge werden bei jeder Nachricht in den Claude-Context injiziert — Cross-Session Gedächtnis für Patterns, Zugänge, Präferenzen (`memory.rs`, `claude.rs`)
|
||||
- **Memory CRUD-Commands**: Speichern, Löschen, Auflisten, Auto-Load-Filter für Memory-Einträge als Tauri-Commands (`memory.rs`, `lib.rs`)
|
||||
- **Global Hotkey (Super+C)**: Claude-Eingabe von überall öffnen — Fenster wird angezeigt, fokussiert, Input-Feld aktiviert (`lib.rs`, `ChatPanel.svelte`)
|
||||
- **Clipboard-Watch**: Überwacht Zwischenablage, erkennt Code/URLs/Fehler/Pfade, zeigt Vorschlag im Chat (`clipboard.rs`, `ChatPanel.svelte`)
|
||||
- **File-Browser für Projekt-Wechsel**: Nativer Verzeichnis-Dialog statt manuellem Pfad, Auto-Name aus Ordnername (`SessionList.svelte`, `tauri-plugin-dialog`)
|
||||
- **Quick-Actions Palette (Ctrl+K)**: VS-Code-artige Kommandopalette mit Suche, Kategorien (Build, Git, Session, Navigation, Voice, Tools), Keyboard-Navigation (`QuickActions.svelte`)
|
||||
- **Lokales Voice (Phase 2.2)**: whisper-cli STT + piper-tts TTS, komplett lokal ohne OpenAI-API (`voice.rs`, `VoicePanel.svelte`)
|
||||
- **Chat-Detach**: Chat in separates Fenster herauslösen, Platz für andere Panels, Zurückholen per Button (`chat_window.rs`, `+page.svelte`)
|
||||
|
|
|
|||
|
|
@ -91,11 +91,11 @@ Alles aus Phase 1-16 ist implementiert und funktionsfaehig:
|
|||
|
||||
| Feature | Datei(en) | Beschreibung |
|
||||
|---------|-----------|--------------|
|
||||
| D-Bus Actions | `programs.rs` | Vordefinierte Aktionen: Dolphin oeffnen, Kate starten, Notifications |
|
||||
| Clipboard-Watch | neu: `clipboard.rs` | Claude reagiert auf Clipboard-Inhalt (Code-Snippet → erklaeren, URL → zusammenfassen) |
|
||||
| D-Bus Actions | `programs.rs` | ⬜ Vordefinierte Aktionen: Dolphin oeffnen, Kate starten, Notifications |
|
||||
| ✅ Clipboard-Watch | `clipboard.rs` | Claude reagiert auf Clipboard-Inhalt (Code/URL/Fehler erkennen) |
|
||||
| ✅ File-Drop | `ChatPanel.svelte` | Dateien auf Chat droppen → Claude analysiert/bearbeitet |
|
||||
| Screenshot-Analyse | `programs.rs` | Bildschirmbereich markieren → Claude beschreibt/debuggt UI |
|
||||
| Global Hotkey | `lib.rs` | Super+C oeffnet Claude-Eingabe von ueberall |
|
||||
| Screenshot-Analyse | `programs.rs` | ⬜ Bildschirmbereich markieren → Claude beschreibt/debuggt UI |
|
||||
| ✅ Global Hotkey | `lib.rs` | Super+C oeffnet Claude-Eingabe von ueberall |
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
67
src-tauri/Cargo.lock
generated
67
src-tauri/Cargo.lock
generated
|
|
@ -491,6 +491,7 @@ dependencies = [
|
|||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-dialog",
|
||||
"tauri-plugin-global-shortcut",
|
||||
"tauri-plugin-shell",
|
||||
"tokio",
|
||||
"tokio-tungstenite",
|
||||
|
|
@ -1410,6 +1411,16 @@ dependencies = [
|
|||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gethostname"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8"
|
||||
dependencies = [
|
||||
"rustix",
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.1.16"
|
||||
|
|
@ -1542,6 +1553,24 @@ version = "0.3.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
||||
|
||||
[[package]]
|
||||
name = "global-hotkey"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9247516746aa8e53411a0db9b62b0e24efbcf6a76e0ba73e5a91b512ddabed7"
|
||||
dependencies = [
|
||||
"crossbeam-channel",
|
||||
"keyboard-types",
|
||||
"objc2",
|
||||
"objc2-app-kit",
|
||||
"once_cell",
|
||||
"serde",
|
||||
"thiserror 2.0.18",
|
||||
"windows-sys 0.59.0",
|
||||
"x11rb",
|
||||
"xkeysym",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gobject-sys"
|
||||
version = "0.18.0"
|
||||
|
|
@ -4705,6 +4734,21 @@ dependencies = [
|
|||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-global-shortcut"
|
||||
version = "2.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "424af23c7e88d05e4a1a6fc2c7be077912f8c76bd7900fd50aa2b7cbf5a2c405"
|
||||
dependencies = [
|
||||
"global-hotkey",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-shell"
|
||||
version = "2.3.5"
|
||||
|
|
@ -6348,6 +6392,29 @@ dependencies = [
|
|||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "x11rb"
|
||||
version = "0.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414"
|
||||
dependencies = [
|
||||
"gethostname",
|
||||
"rustix",
|
||||
"x11rb-protocol",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "x11rb-protocol"
|
||||
version = "0.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd"
|
||||
|
||||
[[package]]
|
||||
name = "xkeysym"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56"
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
version = "0.8.2"
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ tokio-tungstenite = "0.23"
|
|||
futures-util = "0.3"
|
||||
sha2 = "0.10"
|
||||
tauri-plugin-dialog = "2.7.0"
|
||||
tauri-plugin-global-shortcut = "2.3.1"
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
webkit2gtk = "2.0"
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@
|
|||
"shell:allow-spawn",
|
||||
"shell:allow-stdin-write",
|
||||
"shell:allow-kill",
|
||||
"dialog:allow-open"
|
||||
"dialog:allow-open",
|
||||
"global-shortcut:allow-register",
|
||||
"global-shortcut:allow-unregister",
|
||||
"global-shortcut:allow-is-registered"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
168
src-tauri/src/clipboard.rs
Normal file
168
src-tauri/src/clipboard.rs
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
// 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)
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tauri::{
|
||||
Manager,
|
||||
Emitter, Manager,
|
||||
menu::{Menu, MenuItem},
|
||||
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
|
||||
};
|
||||
|
|
@ -13,6 +13,7 @@ use webkit2gtk::WebViewExt;
|
|||
|
||||
mod audit;
|
||||
mod claude;
|
||||
mod clipboard;
|
||||
mod commands;
|
||||
mod context;
|
||||
mod db;
|
||||
|
|
@ -34,9 +35,11 @@ pub fn run() {
|
|||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
|
||||
.manage(Arc::new(Mutex::new(claude::ClaudeState::default())))
|
||||
.manage(guard::GuardState::new(Mutex::new(guard::GuardRails::new())))
|
||||
.manage::<hooks::HookState>(Arc::new(Mutex::new(hooks::HookManager::default())))
|
||||
.manage::<clipboard::ClipboardState>(Arc::new(Mutex::new(clipboard::ClipboardWatcher::default())))
|
||||
.manage::<ide::IdeState>(Arc::new(Mutex::new(ide::IdeConnector::default())))
|
||||
// Phase 2.0: MySQL Pool als Managed State — wird einmal erstellt, von allen Knowledge-Commands geteilt
|
||||
.manage::<knowledge::MysqlPoolState>(knowledge::create_managed_pool())
|
||||
|
|
@ -171,6 +174,10 @@ pub fn run() {
|
|||
update::apply_update,
|
||||
update::apply_bundle_update,
|
||||
update::get_current_version,
|
||||
// Clipboard-Watch
|
||||
clipboard::clipboard_watch_start,
|
||||
clipboard::clipboard_watch_stop,
|
||||
clipboard::clipboard_watch_status,
|
||||
// Slash-Command Registry
|
||||
commands::get_slash_commands,
|
||||
])
|
||||
|
|
@ -263,6 +270,23 @@ pub fn run() {
|
|||
// Lock-Datei erstellen (Instanz-Schutz + Update-Safety)
|
||||
update::create_lock_file();
|
||||
|
||||
// Global Hotkey: Super+C öffnet Claude-Eingabe von überall
|
||||
{
|
||||
use tauri_plugin_global_shortcut::GlobalShortcutExt;
|
||||
let hotkey_handle = app.handle().clone();
|
||||
let _ = app.global_shortcut().on_shortcut("Super+C", move |_app, _shortcut, event| {
|
||||
if event.state == tauri_plugin_global_shortcut::ShortcutState::Pressed {
|
||||
if let Some(window) = hotkey_handle.get_webview_window("main") {
|
||||
let _ = window.show();
|
||||
let _ = window.unminimize();
|
||||
let _ = window.set_focus();
|
||||
let _ = hotkey_handle.emit("focus-chat-input", ());
|
||||
}
|
||||
}
|
||||
});
|
||||
println!("⌨️ Global Hotkey registriert: Super+C");
|
||||
}
|
||||
|
||||
// WebKitGTK: Mikrofon-/Kamera-Permissions automatisch erlauben
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { emit } from '@tauri-apps/api/event';
|
||||
import { emit, listen } from '@tauri-apps/api/event';
|
||||
import { messages, currentInput, isProcessing, addMessage, currentSessionId, messageToDb, queuedMessage, messageQueue, type Message, type QuickAction } from '$lib/stores/app';
|
||||
import { currentTool, processingPhase } from '$lib/stores/events';
|
||||
import { marked, type Tokens } from 'marked';
|
||||
|
|
@ -702,9 +702,21 @@
|
|||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
onMount(async () => {
|
||||
window.addEventListener('keydown', handleGlobalKeydown);
|
||||
|
||||
// Global Hotkey: Super+C fokussiert das Input-Feld
|
||||
const unlistenFocus = await listen('focus-chat-input', () => {
|
||||
inputTextarea?.focus();
|
||||
});
|
||||
|
||||
// Clipboard-Watch Events: Vorschlag im Chat anzeigen
|
||||
const unlistenClipboard = await listen<{ content: string; content_type: string; suggestion: string }>('clipboard-changed', (event) => {
|
||||
const { content, content_type, suggestion } = event.payload;
|
||||
const preview = content.length > 200 ? content.substring(0, 200) + '...' : content;
|
||||
addMessage('system', `📋 Clipboard (${content_type}): ${suggestion}\n\`\`\`\n${preview}\n\`\`\``);
|
||||
});
|
||||
|
||||
// Queue-Auto-Dispatch: sobald isProcessing von true auf false wechselt
|
||||
// wird die naechste Nachricht aus der Queue abgeschickt (FIFO).
|
||||
let lastProcessing = false;
|
||||
|
|
@ -724,6 +736,8 @@
|
|||
return () => {
|
||||
window.removeEventListener('keydown', handleGlobalKeydown);
|
||||
unsubProcessing();
|
||||
unlistenFocus();
|
||||
unlistenClipboard();
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue