use crate::clipboard::LastHash; use crate::db::{ClipboardEntry, Database, Settings}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use tauri::{Manager, State}; pub struct DbState(pub Arc); pub struct PausedState(pub Arc); pub struct LastHashState(pub LastHash); #[tauri::command] pub fn get_entries(db: State<'_, DbState>, limit: Option) -> Result, String> { db.0.get_entries(limit.unwrap_or(crate::db::DISPLAY_LIMIT)) .map_err(|e| e.to_string()) } #[tauri::command] pub fn search_entries(db: State<'_, DbState>, query: String, limit: Option) -> Result, String> { if query.trim().is_empty() { return get_entries(db, limit); } db.0.search_entries(&query, limit.unwrap_or(crate::db::DISPLAY_LIMIT)) .map_err(|e| e.to_string()) } #[tauri::command] pub fn get_entry(db: State<'_, DbState>, id: i64) -> Result { db.0.get_entry(id) .map_err(|e| e.to_string())? .ok_or_else(|| format!("entry {} not found", id)) } #[tauri::command] pub fn latest_entry_id(db: State<'_, DbState>) -> Result, String> { db.0.latest_entry_id().map_err(|e| e.to_string()) } #[tauri::command] pub fn delete_entry(db: State<'_, DbState>, id: i64) -> Result<(), String> { db.0.delete_entry(id).map_err(|e| e.to_string()) } #[tauri::command] pub fn toggle_pin(db: State<'_, DbState>, id: i64) -> Result { db.0.toggle_pin(id).map_err(|e| e.to_string()) } #[tauri::command] pub fn clear_all(db: State<'_, DbState>) -> Result<(), String> { db.0.clear_all().map_err(|e| e.to_string()) } #[tauri::command] pub fn get_settings(db: State<'_, DbState>) -> Result { db.0.get_settings().map_err(|e| e.to_string()) } #[tauri::command] pub fn set_setting(db: State<'_, DbState>, key: String, value: String) -> Result<(), String> { db.0.set_setting(&key, &value).map_err(|e| e.to_string()) } #[tauri::command] pub fn get_paused(paused: State<'_, PausedState>) -> bool { paused.0.load(Ordering::Relaxed) } #[tauri::command] pub fn set_paused(paused: State<'_, PausedState>, value: bool) { paused.0.store(value, Ordering::Relaxed); } #[tauri::command] pub fn save_window_size(db: State<'_, DbState>, width: i64, height: i64) -> Result<(), String> { db.0.set_setting("window_width", &width.to_string()) .map_err(|e| e.to_string())?; db.0.set_setting("window_height", &height.to_string()) .map_err(|e| e.to_string()) } /// Validate, persist, and re-register the global toggle hotkey. /// On registration failure the previous hotkey is restored. #[tauri::command] pub fn set_hotkey( app: tauri::AppHandle, db: State<'_, DbState>, hotkey: String, ) -> Result<(), String> { use tauri_plugin_global_shortcut::GlobalShortcutExt; let new_shortcut = crate::parse_hotkey(&hotkey) .ok_or_else(|| format!("Invalid hotkey: {}", hotkey))?; let old = db.0.get_settings().map_err(|e| e.to_string())?.hotkey; let gs = app.global_shortcut(); if let Some(old_shortcut) = crate::parse_hotkey(&old) { let _ = gs.unregister(old_shortcut); } if let Err(e) = gs.register(new_shortcut) { // Roll back so the app keeps a working hotkey if let Some(old_shortcut) = crate::parse_hotkey(&old) { let _ = gs.register(old_shortcut); } return Err(format!("Could not register '{}': {}", hotkey, e)); } db.0.set_setting("hotkey", &hotkey).map_err(|e| e.to_string()) } /// Write content to the system clipboard and update the shared hash so the /// polling thread doesn't re-insert it. Shared by copy-only and paste-and-refocus. fn write_clipboard( last_hash: &LastHash, content: &str, kind: &str, ) -> Result<(), String> { { let h = crate::clipboard::hash_content(content.as_bytes()); *last_hash.lock().unwrap() = h; } let mut clipboard = arboard::Clipboard::new() .map_err(|e| format!("clipboard open failed: {}", e))?; if kind == "image" && content.starts_with("data:image/") { let (w, h, rgba) = crate::image_util::data_uri_to_rgba(content)?; let img = crate::image_util::to_arboard_image(w, h, rgba); clipboard .set_image(img) .map_err(|e| format!("clipboard image write failed: {}", e))?; } else { clipboard .set_text(content) .map_err(|e| format!("clipboard write failed: {}", e))?; } Ok(()) } /// Copy content to the clipboard without pasting or hiding the window — /// Ditto/Pastebot-style "copy only" for building up a paste elsewhere. #[tauri::command] pub fn copy_to_clipboard( last_hash: State<'_, LastHashState>, content: String, content_type: Option, ) -> Result<(), String> { let kind = content_type.unwrap_or_else(|| { if content.starts_with("data:image/") { "image".into() } else { "text".into() } }); write_clipboard(&last_hash.0, &content, &kind) } /// Write content to the system clipboard, update the shared hash so the /// polling thread doesn't re-insert it, hide the window, then simulate /// Cmd+V via AppleScript to paste into the previously-focused app. #[tauri::command] pub async fn paste_and_refocus( app: tauri::AppHandle, last_hash: State<'_, LastHashState>, content: String, content_type: Option, ) -> Result<(), String> { let kind = content_type.unwrap_or_else(|| { if content.starts_with("data:image/") { "image".into() } else { "text".into() } }); write_clipboard(&last_hash.0, &content, &kind)?; if let Some(window) = app.get_webview_window("main") { let _ = window.hide(); } tokio::time::sleep(std::time::Duration::from_millis(250)).await; #[cfg(target_os = "macos")] { let script = r#" tell application "System Events" set frontApp to name of first application process whose frontmost is true end tell tell application frontApp to activate delay 0.08 tell application "System Events" keystroke "v" using command down end tell "#; let output = tokio::process::Command::new("osascript") .arg("-e") .arg(script) .output() .await .map_err(|e| format!("osascript spawn failed: {}", e))?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); log::warn!("osascript paste failed: {}", stderr); } } Ok(()) }