From 2e5de9dc5e1044d5619ae050e03c8c20ad6b2172 Mon Sep 17 00:00:00 2001 From: Hikari Date: Sat, 24 Jan 2026 13:50:50 -0800 Subject: [PATCH 1/8] feat: add temp file management system for file uploads (#62) Implements the foundation for file upload support by adding a temp file management system that tracks files per conversation. - Add temp_manager.rs module with TempFileManager struct - Add Tauri commands: save_temp_file, register_temp_file, get_temp_files, cleanup_temp_files, cleanup_all_temp_files, cleanup_orphaned_temp_files - Clean up orphaned files from previous sessions on app startup - Clean up temp files when conversation is deleted - Store temp files in /tmp/hikari-uploads/ with unique UUIDs --- src-tauri/src/commands.rs | 75 +++++++++++++++++ src-tauri/src/lib.rs | 18 +++++ src-tauri/src/temp_manager.rs | 139 ++++++++++++++++++++++++++++++++ src/lib/stores/conversations.ts | 6 +- src/lib/tauri.ts | 9 ++- 5 files changed, 243 insertions(+), 4 deletions(-) create mode 100644 src-tauri/src/temp_manager.rs diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 2b3ce85..beeae08 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -1,3 +1,4 @@ +use std::path::PathBuf; use tauri::{AppHandle, State}; use tauri_plugin_http::reqwest; use tauri_plugin_store::StoreExt; @@ -6,6 +7,7 @@ use crate::achievements::{get_achievement_info, load_achievements, AchievementUn use crate::bridge_manager::SharedBridgeManager; use crate::config::{ClaudeStartOptions, HikariConfig}; use crate::stats::UsageStats; +use crate::temp_manager::SharedTempFileManager; const CONFIG_STORE_KEY: &str = "config"; @@ -298,3 +300,76 @@ pub async fn check_for_updates() -> Result { release_notes: latest.body.clone(), }) } + +#[derive(Debug, Clone, serde::Serialize)] +pub struct SavedFileInfo { + pub path: String, + pub filename: String, +} + +#[tauri::command] +pub async fn save_temp_file( + temp_manager: State<'_, SharedTempFileManager>, + conversation_id: String, + data: Vec, + filename: Option, +) -> Result { + let mut manager = temp_manager.lock(); + let path = manager.save_file(&conversation_id, &data, filename.as_deref())?; + + let filename = path + .file_name() + .map(|n| n.to_string_lossy().to_string()) + .unwrap_or_else(|| "unknown".to_string()); + + Ok(SavedFileInfo { + path: path.to_string_lossy().to_string(), + filename, + }) +} + +#[tauri::command] +pub async fn register_temp_file( + temp_manager: State<'_, SharedTempFileManager>, + conversation_id: String, + file_path: String, +) -> Result<(), String> { + let mut manager = temp_manager.lock(); + manager.register_file(&conversation_id, PathBuf::from(file_path)); + Ok(()) +} + +#[tauri::command] +pub async fn get_temp_files( + temp_manager: State<'_, SharedTempFileManager>, + conversation_id: String, +) -> Result, String> { + let manager = temp_manager.lock(); + let files = manager.get_files_for_conversation(&conversation_id); + Ok(files.iter().map(|p| p.to_string_lossy().to_string()).collect()) +} + +#[tauri::command] +pub async fn cleanup_temp_files( + temp_manager: State<'_, SharedTempFileManager>, + conversation_id: String, +) -> Result<(), String> { + let mut manager = temp_manager.lock(); + manager.cleanup_conversation(&conversation_id) +} + +#[tauri::command] +pub async fn cleanup_all_temp_files( + temp_manager: State<'_, SharedTempFileManager>, +) -> Result<(), String> { + let mut manager = temp_manager.lock(); + manager.cleanup_all() +} + +#[tauri::command] +pub async fn cleanup_orphaned_temp_files( + temp_manager: State<'_, SharedTempFileManager>, +) -> Result { + let mut manager = temp_manager.lock(); + manager.cleanup_orphaned_files() +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 685d3da..c402e7f 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -4,6 +4,7 @@ mod commands; mod config; mod notifications; mod stats; +mod temp_manager; mod types; mod vbs_notification; mod windows_toast; @@ -14,6 +15,7 @@ use bridge_manager::create_shared_bridge_manager; use commands::load_saved_achievements; use commands::*; use notifications::*; +use temp_manager::create_shared_temp_manager; use vbs_notification::*; use windows_toast::*; use wsl_notifications::*; @@ -21,6 +23,7 @@ use wsl_notifications::*; #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { let bridge_manager = create_shared_bridge_manager(); + let temp_manager = create_shared_temp_manager().expect("Failed to create temp file manager"); tauri::Builder::default() .plugin(tauri_plugin_dialog::init()) @@ -31,9 +34,18 @@ pub fn run() { .plugin(tauri_plugin_os::init()) .plugin(tauri_plugin_http::init()) .manage(bridge_manager.clone()) + .manage(temp_manager.clone()) .setup(move |app| { // Initialize the app handle in the bridge manager bridge_manager.lock().set_app_handle(app.handle().clone()); + + // Clean up any orphaned temp files from previous sessions + if let Ok(count) = temp_manager.lock().cleanup_orphaned_files() { + if count > 0 { + println!("Cleaned up {} orphaned temp files", count); + } + } + Ok(()) }) .invoke_handler(tauri::generate_handler![ @@ -58,6 +70,12 @@ pub fn run() { validate_directory, list_skills, check_for_updates, + save_temp_file, + register_temp_file, + get_temp_files, + cleanup_temp_files, + cleanup_all_temp_files, + cleanup_orphaned_temp_files, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src-tauri/src/temp_manager.rs b/src-tauri/src/temp_manager.rs new file mode 100644 index 0000000..cfc8cea --- /dev/null +++ b/src-tauri/src/temp_manager.rs @@ -0,0 +1,139 @@ +use parking_lot::Mutex; +use std::collections::HashMap; +use std::fs; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use uuid::Uuid; + +const TEMP_DIR_NAME: &str = "hikari-uploads"; + +pub struct TempFileManager { + base_dir: PathBuf, + files: HashMap>, +} + +impl TempFileManager { + pub fn new() -> Result { + let base_dir = std::env::temp_dir().join(TEMP_DIR_NAME); + + if !base_dir.exists() { + fs::create_dir_all(&base_dir) + .map_err(|e| format!("Failed to create temp directory: {}", e))?; + } + + Ok(TempFileManager { + base_dir, + files: HashMap::new(), + }) + } + + #[allow(dead_code)] + pub fn get_base_dir(&self) -> &Path { + &self.base_dir + } + + pub fn save_file( + &mut self, + conversation_id: &str, + data: &[u8], + original_filename: Option<&str>, + ) -> Result { + let unique_id = Uuid::new_v4(); + let extension = original_filename + .and_then(|name| Path::new(name).extension()) + .and_then(|ext| ext.to_str()) + .unwrap_or("bin"); + + let filename = format!("{}_{}.{}", conversation_id, unique_id, extension); + let file_path = self.base_dir.join(&filename); + + fs::write(&file_path, data) + .map_err(|e| format!("Failed to write temp file: {}", e))?; + + self.files + .entry(conversation_id.to_string()) + .or_default() + .push(file_path.clone()); + + Ok(file_path) + } + + pub fn register_file(&mut self, conversation_id: &str, file_path: PathBuf) { + self.files + .entry(conversation_id.to_string()) + .or_default() + .push(file_path); + } + + pub fn get_files_for_conversation(&self, conversation_id: &str) -> Vec { + self.files + .get(conversation_id) + .cloned() + .unwrap_or_default() + } + + pub fn cleanup_conversation(&mut self, conversation_id: &str) -> Result<(), String> { + if let Some(files) = self.files.remove(conversation_id) { + for file_path in files { + if file_path.exists() { + if let Err(e) = fs::remove_file(&file_path) { + eprintln!( + "Warning: Failed to remove temp file {:?}: {}", + file_path, e + ); + } + } + } + } + Ok(()) + } + + pub fn cleanup_all(&mut self) -> Result<(), String> { + let conversation_ids: Vec = self.files.keys().cloned().collect(); + + for conversation_id in conversation_ids { + self.cleanup_conversation(&conversation_id)?; + } + + Ok(()) + } + + pub fn cleanup_orphaned_files(&mut self) -> Result { + let mut cleaned_count = 0; + + if !self.base_dir.exists() { + return Ok(0); + } + + let tracked_files: std::collections::HashSet = + self.files.values().flatten().cloned().collect(); + + let entries = fs::read_dir(&self.base_dir) + .map_err(|e| format!("Failed to read temp directory: {}", e))?; + + for entry in entries.flatten() { + let path = entry.path(); + if path.is_file() && !tracked_files.contains(&path) { + if let Err(e) = fs::remove_file(&path) { + eprintln!("Warning: Failed to remove orphaned file {:?}: {}", path, e); + } else { + cleaned_count += 1; + } + } + } + + Ok(cleaned_count) + } +} + +impl Default for TempFileManager { + fn default() -> Self { + Self::new().expect("Failed to create TempFileManager") + } +} + +pub type SharedTempFileManager = Arc>; + +pub fn create_shared_temp_manager() -> Result { + Ok(Arc::new(Mutex::new(TempFileManager::new()?))) +} diff --git a/src/lib/stores/conversations.ts b/src/lib/stores/conversations.ts index 6948aaf..243d22a 100644 --- a/src/lib/stores/conversations.ts +++ b/src/lib/stores/conversations.ts @@ -272,7 +272,7 @@ function createConversationsStore() { return newConv.id; }, - deleteConversation: (id: string) => { + deleteConversation: async (id: string) => { ensureInitialized(); const convs = get(conversations); const activeId = get(activeConversationId); @@ -282,8 +282,8 @@ function createConversationsStore() { return false; } - // Clean up tracking for this conversation - cleanupConversationTracking(id); + // Clean up tracking for this conversation (including temp files) + await cleanupConversationTracking(id); conversations.update((c) => { c.delete(id); diff --git a/src/lib/tauri.ts b/src/lib/tauri.ts index a1f7ec5..d14ded8 100644 --- a/src/lib/tauri.ts +++ b/src/lib/tauri.ts @@ -107,8 +107,15 @@ interface WorkingDirectoryPayload { conversation_id?: string; } -export function cleanupConversationTracking(conversationId: string) { +export async function cleanupConversationTracking(conversationId: string) { connectedConversations.delete(conversationId); + + // Clean up any temp files associated with this conversation + try { + await invoke("cleanup_temp_files", { conversationId }); + } catch (error) { + console.error("Failed to cleanup temp files for conversation:", error); + } } export async function initializeTauriListeners() { -- 2.52.0 From d3bab9cbab646899831d57a40e786701fa2d5e92 Mon Sep 17 00:00:00 2001 From: Hikari Date: Sat, 24 Jan 2026 13:57:51 -0800 Subject: [PATCH 2/8] feat: add attachment preview UI component (#66) - Create Attachment interface in messages.ts - Create AttachmentPreview.svelte component with: - Image thumbnails for image attachments - File icons for documents - File size display - Remove button on hover - Add attachments array to Conversation interface - Add attachment management methods to conversation store: - addAttachment, removeAttachment, clearAttachments, getAttachments - Integrate AttachmentPreview into InputBar - Clear attachments on conversation reset --- src/lib/components/AttachmentPreview.svelte | 204 ++++++++++++++++++++ src/lib/components/InputBar.svelte | 13 ++ src/lib/stores/claude.ts | 8 + src/lib/stores/conversations.ts | 57 ++++++ src/lib/types/messages.ts | 10 + 5 files changed, 292 insertions(+) create mode 100644 src/lib/components/AttachmentPreview.svelte diff --git a/src/lib/components/AttachmentPreview.svelte b/src/lib/components/AttachmentPreview.svelte new file mode 100644 index 0000000..c12d1e7 --- /dev/null +++ b/src/lib/components/AttachmentPreview.svelte @@ -0,0 +1,204 @@ + + +{#if attachments.length > 0} +
+
+ {attachments.length} attachment{attachments.length !== 1 ? "s" : ""} +
+
+ {#each attachments as attachment (attachment.id)} +
+ {#if attachment.type === "image" && attachment.previewUrl} +
+ {attachment.filename} +
+ {:else} +
+ {getFileIcon(attachment.type)} +
+ {/if} +
+ + {attachment.filename} + + + {formatFileSize(attachment.size)} + +
+ +
+ {/each} +
+
+{/if} + + diff --git a/src/lib/components/InputBar.svelte b/src/lib/components/InputBar.svelte index 7313c06..166e047 100644 --- a/src/lib/components/InputBar.svelte +++ b/src/lib/components/InputBar.svelte @@ -22,6 +22,8 @@ isSlashCommand, type SlashCommand, } from "$lib/commands/slashCommands"; + import AttachmentPreview from "$lib/components/AttachmentPreview.svelte"; + import type { Attachment } from "$lib/types/messages"; const INPUT_HISTORY_KEY = "hikari-input-history"; const MAX_HISTORY_SIZE = 100; @@ -33,6 +35,7 @@ let showCommandMenu = $state(false); let matchingCommands = $state([]); let selectedCommandIndex = $state(0); + let attachments = $state([]); // Input history state let inputHistory = $state([]); @@ -112,6 +115,10 @@ isProcessing = processing; }); + claudeStore.attachments.subscribe((storedAttachments) => { + attachments = storedAttachments; + }); + function handleInputChange() { // If input is empty, allow history navigation again // Otherwise, mark that user has manually typed @@ -289,6 +296,10 @@ User: ${formattedMessage}`; } } + function handleRemoveAttachment(id: string) { + claudeStore.removeAttachment(id); + } + function handleKeyDown(event: KeyboardEvent) { // Handle command menu navigation if (showCommandMenu && matchingCommands.length > 0) { @@ -353,6 +364,8 @@ User: ${formattedMessage}`;
+ +
diff --git a/src/lib/stores/claude.ts b/src/lib/stores/claude.ts index 7b8983e..c361e46 100644 --- a/src/lib/stores/claude.ts +++ b/src/lib/stores/claude.ts @@ -25,6 +25,7 @@ export const claudeStore = { isProcessing: conversationsStore.isProcessing, grantedTools: conversationsStore.grantedTools, pendingRetryMessage: conversationsStore.pendingRetryMessage, + attachments: conversationsStore.attachments, // New conversation-aware subscriptions conversations: conversationsStore.conversations, @@ -67,6 +68,12 @@ export const claudeStore = { saveScrollPosition: conversationsStore.saveScrollPosition, getScrollPosition: conversationsStore.getScrollPosition, + // Attachment management + addAttachment: conversationsStore.addAttachment, + removeAttachment: conversationsStore.removeAttachment, + clearAttachments: conversationsStore.clearAttachments, + getAttachments: conversationsStore.getAttachments, + getGrantedTools: (): string[] => { let tools: string[] = []; conversationsStore.grantedTools.subscribe((t) => (tools = Array.from(t)))(); @@ -86,6 +93,7 @@ export const claudeStore = { conversationsStore.setWorkingDirectory(""); conversationsStore.setProcessing(false); conversationsStore.revokeAllTools(); + conversationsStore.clearAttachments(); // Also clear history restoration clearHistoryRestore(); }, diff --git a/src/lib/stores/conversations.ts b/src/lib/stores/conversations.ts index 243d22a..7480f8c 100644 --- a/src/lib/stores/conversations.ts +++ b/src/lib/stores/conversations.ts @@ -4,6 +4,7 @@ import type { ConnectionStatus, PermissionRequest, UserQuestionEvent, + Attachment, } from "$lib/types/messages"; import type { CharacterState } from "$lib/types/states"; import { cleanupConversationTracking } from "$lib/tauri"; @@ -24,6 +25,7 @@ export interface Conversation { scrollPosition: number; createdAt: Date; lastActivityAt: Date; + attachments: Attachment[]; } function createConversationsStore() { @@ -59,6 +61,7 @@ function createConversationsStore() { scrollPosition: -1, // -1 means "scroll to bottom" (auto-scroll) createdAt: new Date(), lastActivityAt: new Date(), + attachments: [], }; } @@ -109,6 +112,7 @@ function createConversationsStore() { ); const pendingQuestion = derived(activeConversation, ($conv) => $conv?.pendingQuestion || null); const scrollPosition = derived(activeConversation, ($conv) => $conv?.scrollPosition ?? -1); + const attachments = derived(activeConversation, ($conv) => $conv?.attachments || []); return { // Expose derived stores for compatibility @@ -122,6 +126,7 @@ function createConversationsStore() { grantedTools: { subscribe: grantedTools.subscribe }, pendingRetryMessage: { subscribe: pendingRetryMessage.subscribe }, scrollPosition: { subscribe: scrollPosition.subscribe }, + attachments: { subscribe: attachments.subscribe }, // New conversation-specific stores conversations: { subscribe: conversations.subscribe }, @@ -571,6 +576,58 @@ function createConversationsStore() { return conv?.grantedTools.has(toolName) || false; }, + // Attachment management + addAttachment: (attachment: Attachment) => { + const activeId = get(activeConversationId); + if (!activeId) return; + + conversations.update((convs) => { + const conv = convs.get(activeId); + if (conv) { + conv.attachments.push(attachment); + conv.lastActivityAt = new Date(); + } + return convs; + }); + }, + + removeAttachment: (id: string) => { + const activeId = get(activeConversationId); + if (!activeId) return; + + conversations.update((convs) => { + const conv = convs.get(activeId); + if (conv) { + conv.attachments = conv.attachments.filter((a) => a.id !== id); + conv.lastActivityAt = new Date(); + } + return convs; + }); + }, + + clearAttachments: () => { + const activeId = get(activeConversationId); + if (!activeId) return; + + conversations.update((convs) => { + const conv = convs.get(activeId); + if (conv) { + conv.attachments = []; + conv.lastActivityAt = new Date(); + } + return convs; + }); + }, + + getAttachments: (): Attachment[] => { + const activeId = get(activeConversationId); + if (!activeId) return []; + + const convs = get(conversations); + const conv = convs.get(activeId); + return conv?.attachments || []; + }, + // Add initialization helper initialize: () => { ensureInitialized(); diff --git a/src/lib/types/messages.ts b/src/lib/types/messages.ts index 2985bd0..8e85797 100644 --- a/src/lib/types/messages.ts +++ b/src/lib/types/messages.ts @@ -142,6 +142,16 @@ export interface UserQuestionEvent { export type ConnectionStatus = "disconnected" | "connecting" | "connected" | "error"; +export interface Attachment { + id: string; + filename: string; + path: string; + size: number; + type: "image" | "document" | "other"; + mimeType?: string; + previewUrl?: string; // For images, a data URL or object URL for preview +} + export interface UpdateInfo { current_version: string; latest_version: string; -- 2.52.0 From a191bdef239d964bedfdd466c357aff5b4a7e47e Mon Sep 17 00:00:00 2001 From: Hikari Date: Sat, 24 Jan 2026 14:01:01 -0800 Subject: [PATCH 3/8] feat: add file picker button for attachments (#63) --- src/lib/components/InputBar.svelte | 129 +++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) diff --git a/src/lib/components/InputBar.svelte b/src/lib/components/InputBar.svelte index 166e047..ca06f0f 100644 --- a/src/lib/components/InputBar.svelte +++ b/src/lib/components/InputBar.svelte @@ -1,5 +1,6 @@ - +
@@ -532,6 +600,33 @@ User: ${formattedMessage}`; display: flex; flex-direction: column; gap: 8px; + position: relative; + transition: border-color 0.2s, background 0.2s; + } + + .input-bar.is-dragging { + background: var(--bg-secondary); + border: 2px dashed var(--accent-primary); + border-radius: 12px; + padding: 8px; + } + + .input-bar.is-dragging::before { + content: "Drop files here"; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-size: 18px; + font-weight: 500; + color: var(--accent-primary); + pointer-events: none; + z-index: 10; + } + + .input-bar.is-dragging > * { + opacity: 0.3; + pointer-events: none; } .input-controls { -- 2.52.0 From a183a7ef475afc4c36ad0db2376079e6bf0b8eed Mon Sep 17 00:00:00 2001 From: Hikari Date: Sat, 24 Jan 2026 14:03:45 -0800 Subject: [PATCH 5/8] feat: add clipboard paste file support (#65) --- src/lib/components/InputBar.svelte | 39 ++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/lib/components/InputBar.svelte b/src/lib/components/InputBar.svelte index 998e084..7c6c4aa 100644 --- a/src/lib/components/InputBar.svelte +++ b/src/lib/components/InputBar.svelte @@ -441,6 +441,44 @@ User: ${formattedMessage}`; } } + function handlePaste(event: ClipboardEvent) { + const items = event.clipboardData?.items; + if (!items) return; + + for (const item of items) { + // Check if the item is a file (image or other) + if (item.kind === "file") { + const file = item.getAsFile(); + if (!file) continue; + + // Prevent default for file pastes so we handle it + event.preventDefault(); + + const filename = file.name || `pasted-${Date.now()}.${file.type.split("/")[1] || "png"}`; + const extension = filename.split(".").pop()?.toLowerCase() || ""; + const fileType = getFileTypeFromExtension(extension); + + // Create preview URL for images + let previewUrl: string | undefined; + if (fileType === "image" || file.type.startsWith("image/")) { + previewUrl = URL.createObjectURL(file); + } + + const attachment: Attachment = { + id: `attachment-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, + filename, + path: filename, + size: file.size, + type: file.type.startsWith("image/") ? "image" : fileType, + mimeType: file.type, + previewUrl, + }; + + claudeStore.addAttachment(attachment); + } + } + } + function handleKeyDown(event: KeyboardEvent) { // Handle command menu navigation if (showCommandMenu && matchingCommands.length > 0) { @@ -532,6 +570,7 @@ User: ${formattedMessage}`; bind:value={inputValue} onkeydown={handleKeyDown} oninput={handleInputChange} + onpaste={handlePaste} placeholder={isConnected ? "Ask Hikari anything... (type / for commands)" : "Connect to Claude first..."} -- 2.52.0 From 07f9cf8c30f624b62952fb5d305202395c8b20e6 Mon Sep 17 00:00:00 2001 From: Naomi Carrigan Date: Sat, 24 Jan 2026 16:10:32 -0800 Subject: [PATCH 6/8] feat: add native clipboard support for screenshot paste MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add Tauri clipboard-manager plugin for native clipboard access - Fall back to native clipboard when WebView clipboard API returns empty - Convert RGBA image data to PNG via canvas for saving - Allow sending messages with attachments only (no text required) - Log attached files to output with 📎 emoji - Update send button to enable when attachments exist Co-Authored-By: Naomi Carrigan --- package.json | 1 + pnpm-lock.yaml | 10 + src-tauri/Cargo.lock | 355 +++++++++++++++++++++++++++- src-tauri/Cargo.toml | 1 + src-tauri/capabilities/default.json | 4 +- src-tauri/src/lib.rs | 1 + src/lib/components/InputBar.svelte | 204 +++++++++++++--- 7 files changed, 534 insertions(+), 42 deletions(-) diff --git a/package.json b/package.json index 5509669..42fca6d 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "license": "MIT", "dependencies": { "@tauri-apps/api": "^2", + "@tauri-apps/plugin-clipboard-manager": "^2.3.2", "@tauri-apps/plugin-dialog": "^2", "@tauri-apps/plugin-notification": "^2", "@tauri-apps/plugin-opener": "^2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 95070cc..2bc5ddd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@tauri-apps/api': specifier: ^2 version: 2.9.1 + '@tauri-apps/plugin-clipboard-manager': + specifier: ^2.3.2 + version: 2.3.2 '@tauri-apps/plugin-dialog': specifier: ^2 version: 2.6.0 @@ -735,6 +738,9 @@ packages: engines: {node: '>= 10'} hasBin: true + '@tauri-apps/plugin-clipboard-manager@2.3.2': + resolution: {integrity: sha512-CUlb5Hqi2oZbcZf4VUyUH53XWPPdtpw43EUpCza5HWZJwxEoDowFzNUDt1tRUXA8Uq+XPn17Ysfptip33sG4eQ==} + '@tauri-apps/plugin-dialog@2.6.0': resolution: {integrity: sha512-q4Uq3eY87TdcYzXACiYSPhmpBA76shgmQswGkSVio4C82Sz2W4iehe9TnKYwbq7weHiL88Yw19XZm7v28+Micg==} @@ -2286,6 +2292,10 @@ snapshots: '@tauri-apps/cli-win32-ia32-msvc': 2.9.6 '@tauri-apps/cli-win32-x64-msvc': 2.9.6 + '@tauri-apps/plugin-clipboard-manager@2.3.2': + dependencies: + '@tauri-apps/api': 2.9.1 + '@tauri-apps/plugin-dialog@2.6.0': dependencies: '@tauri-apps/api': 2.9.1 diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 85f26b9..3ed5bd2 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -47,6 +47,27 @@ version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +[[package]] +name = "arboard" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" +dependencies = [ + "clipboard-win", + "image", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "parking_lot", + "percent-encoding", + "windows-sys 0.60.2", + "wl-clipboard-rs", + "x11rb", +] + [[package]] name = "async-broadcast" version = "0.7.2" @@ -310,6 +331,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "bytes" version = "1.11.0" @@ -449,6 +476,15 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "clipboard-win" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" +dependencies = [ + "error-code", +] + [[package]] name = "combine" version = "4.6.7" @@ -604,6 +640,12 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "crypto-common" version = "0.1.7" @@ -807,6 +849,12 @@ dependencies = [ "litrs", ] +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + [[package]] name = "dpi" version = "0.1.2" @@ -926,6 +974,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "error-code" +version = "3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" + [[package]] name = "event-listener" version = "5.4.1" @@ -953,6 +1007,26 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "fax" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" +dependencies = [ + "fax_derive", +] + +[[package]] +name = "fax_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "fdeflate" version = "0.3.7" @@ -978,6 +1052,12 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41" +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + [[package]] name = "flate2" version = "1.1.8" @@ -994,6 +1074,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "foreign-types" version = "0.5.0" @@ -1458,12 +1544,32 @@ dependencies = [ "tracing", ] +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + [[package]] name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + [[package]] name = "hashbrown" version = "0.16.1" @@ -1496,7 +1602,7 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hikari-desktop" -version = "0.2.0" +version = "0.3.0" dependencies = [ "chrono", "parking_lot", @@ -1505,6 +1611,7 @@ dependencies = [ "serde_json", "tauri", "tauri-build", + "tauri-plugin-clipboard-manager", "tauri-plugin-dialog", "tauri-plugin-http", "tauri-plugin-notification", @@ -1665,7 +1772,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc50b891e4acf8fe0e71ef88ec43ad82ee07b3810ad09de10f1d01f072ed4b98" dependencies = [ "byteorder", - "png", + "png 0.17.16", ] [[package]] @@ -1776,6 +1883,20 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "image" +version = "0.25.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a" +dependencies = [ + "bytemuck", + "byteorder-lite", + "moxcms", + "num-traits", + "png 0.18.0", + "tiff", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -2135,6 +2256,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "moxcms" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97" +dependencies = [ + "num-traits", + "pxfm", +] + [[package]] name = "muda" version = "0.17.1" @@ -2150,7 +2281,7 @@ dependencies = [ "objc2-core-foundation", "objc2-foundation", "once_cell", - "png", + "png 0.17.16", "serde", "thiserror 2.0.17", "windows-sys 0.60.2", @@ -2210,6 +2341,15 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + [[package]] name = "notify-rust" version = "4.11.7" @@ -2628,6 +2768,17 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "petgraph" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" +dependencies = [ + "fixedbitset", + "hashbrown 0.15.5", + "indexmap 2.13.0", +] + [[package]] name = "phf" version = "0.8.0" @@ -2817,6 +2968,19 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "png" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" +dependencies = [ + "bitflags 2.10.0", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "polling" version = "3.11.0" @@ -2945,6 +3109,21 @@ dependencies = [ "psl-types", ] +[[package]] +name = "pxfm" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7186d3822593aa4393561d186d1393b3923e9d6163d3fbfd6e825e3e6cf3e6a8" +dependencies = [ + "num-traits", +] + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quick-xml" version = "0.37.5" @@ -4068,7 +4247,7 @@ dependencies = [ "ico", "json-patch", "plist", - "png", + "png 0.17.16", "proc-macro2", "quote", "semver", @@ -4115,6 +4294,21 @@ dependencies = [ "walkdir", ] +[[package]] +name = "tauri-plugin-clipboard-manager" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "206dc20af4ed210748ba945c2774e60fd0acd52b9a73a028402caf809e9b6ecf" +dependencies = [ + "arboard", + "log", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.17", +] + [[package]] name = "tauri-plugin-dialog" version = "2.6.0" @@ -4452,6 +4646,20 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "tiff" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error", + "weezl", + "zune-jpeg", +] + [[package]] name = "time" version = "0.3.45" @@ -4747,12 +4955,23 @@ dependencies = [ "objc2-core-graphics", "objc2-foundation", "once_cell", - "png", + "png 0.17.16", "serde", "thiserror 2.0.17", "windows-sys 0.60.2", ] +[[package]] +name = "tree_magic_mini" +version = "3.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8765b90061cba6c22b5831f675da109ae5561588290f9fa2317adab2714d5a6" +dependencies = [ + "memchr", + "nom", + "petgraph", +] + [[package]] name = "try-lock" version = "0.2.5" @@ -5034,6 +5253,76 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wayland-backend" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fee64194ccd96bf648f42a65a7e589547096dfa702f7cadef84347b66ad164f9" +dependencies = [ + "cc", + "downcast-rs", + "rustix", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e6faa537fbb6c186cb9f1d41f2f811a4120d1b57ec61f50da451a0c5122bec" +dependencies = [ + "bitflags 2.10.0", + "rustix", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baeda9ffbcfc8cd6ddaade385eaf2393bd2115a69523c735f12242353c3df4f3" +dependencies = [ + "bitflags 2.10.0", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9597cdf02cf0c34cd5823786dce6b5ae8598f05c2daf5621b6e178d4f7345f3" +dependencies = [ + "bitflags 2.10.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5423e94b6a63e68e439803a3e153a9252d5ead12fd853334e2ad33997e3889e3" +dependencies = [ + "proc-macro2", + "quick-xml 0.38.4", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6dbfc3ac5ef974c92a2235805cc0114033018ae1290a72e474aa8b28cbbdfd" +dependencies = [ + "pkg-config", +] + [[package]] name = "web-sys" version = "0.3.85" @@ -5143,6 +5432,12 @@ dependencies = [ "windows-core 0.61.2", ] +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + [[package]] name = "winapi" version = "0.3.9" @@ -5673,6 +5968,24 @@ version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +[[package]] +name = "wl-clipboard-rs" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9651471a32e87d96ef3a127715382b2d11cc7c8bb9822ded8a7cc94072eb0a3" +dependencies = [ + "libc", + "log", + "os_pipe", + "rustix", + "thiserror 2.0.17", + "tree_magic_mini", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-wlr", +] + [[package]] name = "writeable" version = "0.6.2" @@ -5745,6 +6058,23 @@ 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 = "yoke" version = "0.8.1" @@ -5915,6 +6245,21 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd8f3f50b848df28f887acb68e41201b5aea6bc8a8dacc00fb40635ff9a72fea" +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + +[[package]] +name = "zune-jpeg" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" +dependencies = [ + "zune-core", +] + [[package]] name = "zvariant" version = "5.9.1" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 5c3573e..5ac4de4 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -26,6 +26,7 @@ tauri-plugin-store = "2.4.2" tauri-plugin-notification = "2" tauri-plugin-os = "2" tauri-plugin-http = "2" +tauri-plugin-clipboard-manager = "2" tempfile = "3" semver = "1" chrono = { version = "0.4.43", features = ["serde"] } diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 5e2db1c..5e067ad 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -13,6 +13,8 @@ "notification:default", "notification:allow-is-permission-granted", "notification:allow-request-permission", - "notification:allow-notify" + "notification:allow-notify", + "clipboard-manager:default", + "clipboard-manager:allow-read-image" ] } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index c402e7f..4103f5d 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -33,6 +33,7 @@ pub fn run() { .plugin(tauri_plugin_notification::init()) .plugin(tauri_plugin_os::init()) .plugin(tauri_plugin_http::init()) + .plugin(tauri_plugin_clipboard_manager::init()) .manage(bridge_manager.clone()) .manage(temp_manager.clone()) .setup(move |app| { diff --git a/src/lib/components/InputBar.svelte b/src/lib/components/InputBar.svelte index 7c6c4aa..e3e73d3 100644 --- a/src/lib/components/InputBar.svelte +++ b/src/lib/components/InputBar.svelte @@ -1,6 +1,7 @@