diff --git a/frontend/bun.lockb b/frontend/bun.lockb index 3e7fff7..170eb8b 100755 Binary files a/frontend/bun.lockb and b/frontend/bun.lockb differ diff --git a/frontend/package.json b/frontend/package.json index ff969af..dc290f7 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,43 +1,45 @@ { - "name": "haystack", - "version": "0.1.0", - "description": "Screenshots that organize themselves", - "type": "module", - "scripts": { - "start": "vite", - "dev": "vite", - "build": "vite build", - "serve": "vite preview", - "tauri": "tauri", - "lint": "bunx @biomejs/biome lint .", - "format": "bunx @biomejs/biome format . --write" - }, - "license": "MIT", - "dependencies": { - "@kobalte/core": "^0.13.9", - "@kobalte/tailwindcss": "^0.9.0", - "@solidjs/router": "^0.15.3", - "@tabler/icons-solidjs": "^3.30.0", - "@tauri-apps/api": "^2", - "@tauri-apps/plugin-dialog": "~2", - "@tauri-apps/plugin-opener": "^2", - "clsx": "^2.1.1", - "fuse.js": "^7.1.0", - "jwt-decode": "^4.0.0", - "solid-js": "^1.9.3", - "solid-motionone": "^1.0.3", - "tailwind-scrollbar-hide": "^2.0.0", - "valibot": "^1.0.0-rc.2" - }, - "devDependencies": { - "@biomejs/biome": "^1.9.4", - "@tauri-apps/cli": "^2", - "autoprefixer": "^10.4.20", - "postcss": "^8.5.3", - "postcss-cli": "^11.0.0", - "tailwindcss": "3.4.0", - "typescript": "~5.6.2", - "vite": "^6.0.3", - "vite-plugin-solid": "^2.11.0" - } + "name": "haystack", + "version": "0.1.0", + "description": "Screenshots that organize themselves", + "type": "module", + "scripts": { + "start": "vite", + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "tauri": "tauri", + "lint": "bunx @biomejs/biome lint .", + "format": "bunx @biomejs/biome format . --write" + }, + "license": "MIT", + "dependencies": { + "@kobalte/core": "^0.13.9", + "@kobalte/tailwindcss": "^0.9.0", + "@solidjs/router": "^0.15.3", + "@tabler/icons-solidjs": "^3.30.0", + "@tauri-apps/api": "^2", + "@tauri-apps/plugin-dialog": "~2", + "@tauri-apps/plugin-global-shortcut": "~2", + "@tauri-apps/plugin-opener": "^2", + "@tauri-apps/plugin-store": "~2", + "clsx": "^2.1.1", + "fuse.js": "^7.1.0", + "jwt-decode": "^4.0.0", + "solid-js": "^1.9.3", + "solid-motionone": "^1.0.3", + "tailwind-scrollbar-hide": "^2.0.0", + "valibot": "^1.0.0-rc.2" + }, + "devDependencies": { + "@biomejs/biome": "^1.9.4", + "@tauri-apps/cli": "^2", + "autoprefixer": "^10.4.20", + "postcss": "^8.5.3", + "postcss-cli": "^11.0.0", + "tailwindcss": "3.4.0", + "typescript": "~5.6.2", + "vite": "^6.0.3", + "vite-plugin-solid": "^2.11.0" + } } diff --git a/frontend/src-tauri/Cargo.lock b/frontend/src-tauri/Cargo.lock index 9a599d4..887d2cb 100644 --- a/frontend/src-tauri/Cargo.lock +++ b/frontend/src-tauri/Cargo.lock @@ -14,7 +14,9 @@ dependencies = [ "tauri", "tauri-build", "tauri-plugin-dialog", + "tauri-plugin-global-shortcut", "tauri-plugin-opener", + "tauri-plugin-store", "tokio", ] @@ -1459,6 +1461,23 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +[[package]] +name = "global-hotkey" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fbb3a4e56c901ee66c190fdb3fa08344e6d09593cc6c61f8eb9add7144b271" +dependencies = [ + "crossbeam-channel", + "keyboard-types", + "objc2 0.6.0", + "objc2-app-kit 0.3.0", + "once_cell", + "serde", + "thiserror 2.0.11", + "windows-sys 0.59.0", + "x11-dl", +] + [[package]] name = "gobject-sys" version = "0.18.0" @@ -4123,6 +4142,21 @@ dependencies = [ "uuid", ] +[[package]] +name = "tauri-plugin-global-shortcut" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00f646a09511e8d283267dcdaa08c2ef27c4116bf271d9114849d9ca215606c3" +dependencies = [ + "global-hotkey", + "log", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.11", +] + [[package]] name = "tauri-plugin-opener" version = "2.2.5" @@ -4145,6 +4179,22 @@ dependencies = [ "zbus", ] +[[package]] +name = "tauri-plugin-store" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c0c08fae6995909f5e9a0da6038273b750221319f2c0f3b526d6de1cde21505" +dependencies = [ + "dunce", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.11", + "tokio", + "tracing", +] + [[package]] name = "tauri-runtime" version = "2.3.0" diff --git a/frontend/src-tauri/Cargo.toml b/frontend/src-tauri/Cargo.toml index df37364..7bf2860 100644 --- a/frontend/src-tauri/Cargo.toml +++ b/frontend/src-tauri/Cargo.toml @@ -26,6 +26,10 @@ tauri-plugin-dialog = "2" notify = "6.1.1" base64 = "0.21.7" tokio = { version = "1.36.0", features = ["full"] } +tauri-plugin-store = "2" [target."cfg(target_os = \"macos\")".dependencies] cocoa = "0.26" + +[target."cfg(not(any(target_os = \"android\", target_os = \"ios\")))".dependencies] +tauri-plugin-global-shortcut = "2" diff --git a/frontend/src-tauri/capabilities/default.json b/frontend/src-tauri/capabilities/default.json index d21ade3..e7874eb 100644 --- a/frontend/src-tauri/capabilities/default.json +++ b/frontend/src-tauri/capabilities/default.json @@ -7,6 +7,10 @@ "core:default", "opener:default", "dialog:default", - "core:window:allow-start-dragging" + "core:window:allow-start-dragging", + "global-shortcut:allow-is-registered", + "global-shortcut:allow-register", + "global-shortcut:allow-unregister", + "global-shortcut:allow-unregister-all" ] } diff --git a/frontend/src-tauri/src/lib.rs b/frontend/src-tauri/src/lib.rs index 964adee..8fd4e3c 100644 --- a/frontend/src-tauri/src/lib.rs +++ b/frontend/src-tauri/src/lib.rs @@ -1,4 +1,5 @@ mod commands; +mod shortcut; mod state; mod utils; mod window; @@ -11,12 +12,20 @@ pub fn run() { let watcher_state = new_shared_watcher_state(); tauri::Builder::default() + .plugin(tauri_plugin_store::Builder::new().build()) .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_opener::init()) .manage(watcher_state) - .invoke_handler(tauri::generate_handler![commands::handle_selected_folder]) + .invoke_handler(tauri::generate_handler![ + commands::handle_selected_folder, + shortcut::change_shortcut, + shortcut::unregister_shortcut, + shortcut::get_current_shortcut, + ]) .setup(|app| { setup_window(app)?; + shortcut::enable_shortcut(app); + Ok(()) }) .run(tauri::generate_context!()) diff --git a/frontend/src-tauri/src/shortcut.rs b/frontend/src-tauri/src/shortcut.rs new file mode 100644 index 0000000..1032f91 --- /dev/null +++ b/frontend/src-tauri/src/shortcut.rs @@ -0,0 +1,167 @@ +use tauri::App; +use tauri::AppHandle; +use tauri::Manager; +use tauri::Runtime; +use tauri_plugin_global_shortcut::GlobalShortcutExt; +use tauri_plugin_global_shortcut::Shortcut; +use tauri_plugin_global_shortcut::ShortcutState; +use tauri_plugin_store::JsonValue; +use tauri_plugin_store::StoreExt; + +/// Name of the Tauri storage +const HAYSTACK_TAURI_STORE: &str = "haystack_tauri_store"; + +/// Key for storing global shortcuts +const HAYSTACK_GLOBAL_SHORTCUT: &str = "haystack_global_shortcut"; + +/// Default shortcut for macOS +#[cfg(target_os = "macos")] +const DEFAULT_SHORTCUT: &str = "command+shift+k"; + +/// Default shortcut for Windows and Linux +#[cfg(any(target_os = "windows", target_os = "linux"))] +const DEFAULT_SHORTCUT: &str = "ctrl+shift+k"; + +/// Set shortcut during application startup +pub fn enable_shortcut(app: &App) { + let store = app + .store(HAYSTACK_TAURI_STORE) + .expect("Creating the store should not fail"); + + // Use stored shortcut if it exists + if let Some(stored_shortcut) = store.get(HAYSTACK_GLOBAL_SHORTCUT) { + let stored_shortcut_str = match stored_shortcut { + JsonValue::String(str) => str, + unexpected_type => panic!( + "Haystack shortcuts should be stored as strings, found type: {} ", + unexpected_type + ), + }; + let stored_shortcut = stored_shortcut_str + .parse::() + .expect("Stored shortcut string should be valid"); + _register_shortcut_upon_start(app, stored_shortcut); // Register stored shortcut + } else { + // Use default shortcut if none is stored + store.set( + HAYSTACK_GLOBAL_SHORTCUT, + JsonValue::String(DEFAULT_SHORTCUT.to_string()), + ); + let default_shortcut = DEFAULT_SHORTCUT + .parse::() + .expect("Default shortcut should be valid"); + _register_shortcut_upon_start(app, default_shortcut); // Register default shortcut + } +} + +/// Get the current stored shortcut as a string +#[tauri::command] +pub fn get_current_shortcut(app: AppHandle) -> Result { + let shortcut = _get_shortcut(&app); + Ok(shortcut) +} + +/// Unregister the current shortcut in Tauri +#[tauri::command] +pub fn unregister_shortcut(app: AppHandle) { + let shortcut_str = _get_shortcut(&app); + let shortcut = shortcut_str + .parse::() + .expect("Stored shortcut string should be valid"); + + // Unregister the shortcut + app.global_shortcut() + .unregister(shortcut) + .expect("Failed to unregister shortcut") +} + +/// Change the global shortcut +#[tauri::command] +pub fn change_shortcut( + app: AppHandle, + _window: tauri::Window, + key: String, +) -> Result<(), String> { + println!("Key: {}", key); + let shortcut = match key.parse::() { + Ok(shortcut) => shortcut, + Err(_) => return Err(format!("Invalid shortcut {}", key)), + }; + + // Store the new shortcut + let store = app + .get_store(HAYSTACK_TAURI_STORE) + .expect("Store should already be loaded or created"); + store.set(HAYSTACK_GLOBAL_SHORTCUT, JsonValue::String(key)); + + // Register the new shortcut + _register_shortcut(&app, shortcut); + + Ok(()) +} + +/// Helper function to register a shortcut, primarily for updating shortcuts +fn _register_shortcut(app: &AppHandle, shortcut: Shortcut) { + let main_window = app.get_webview_window("main").unwrap(); + // Register global shortcut and define its behavior + app.global_shortcut() + .on_shortcut(shortcut, move |_app, scut, event| { + if scut == &shortcut { + if let ShortcutState::Pressed = event.state() { + // Toggle window visibility + if main_window.is_visible().unwrap() { + main_window.hide().unwrap(); // Hide window + } else { + main_window.show().unwrap(); // Show window + main_window.set_focus().unwrap(); // Focus window + } + } + } + }) + .map_err(|err| format!("Failed to register new shortcut '{}'", err)) + .unwrap(); +} + +/// Helper function to register shortcuts during application startup +fn _register_shortcut_upon_start(app: &App, shortcut: Shortcut) { + let window = app.get_webview_window("main").unwrap(); + // Initialize global shortcut and set its handler + app.handle() + .plugin( + tauri_plugin_global_shortcut::Builder::new() + .with_handler(move |_app, scut, event| { + if scut == &shortcut { + if let ShortcutState::Pressed = event.state() { + // Toggle window visibility + if window.is_visible().unwrap() { + window.hide().unwrap(); // Hide window + } else { + window.show().unwrap(); // Show window + window.set_focus().unwrap(); // Focus window + } + } + } + }) + .build(), + ) + .unwrap(); + app.global_shortcut().register(shortcut).unwrap(); // Register global shortcut +} + +/// Retrieve the stored global shortcut as a string +pub fn _get_shortcut(app: &AppHandle) -> String { + let store = app + .get_store(HAYSTACK_TAURI_STORE) + .expect("Store should already be loaded or created"); + + match store + .get(HAYSTACK_GLOBAL_SHORTCUT) + .expect("Shortcut should already be stored") + { + JsonValue::String(str) => str, + unexpected_type => panic!( + "Haystack shortcuts should be stored as strings, found type: {} ", + unexpected_type + ), + } +}