From 4a5d965ce645f211c84207a274ae063f9364d91e Mon Sep 17 00:00:00 2001 From: Y7 <114366310+hl9020@users.noreply.github.com> Date: Mon, 3 Aug 2026 15:41:14 +0200 Subject: [PATCH] feat(autostart): launch at login and start hidden in the tray --- src-tauri/Cargo.lock | 82 +++++++++++++++++++++++++++--- src-tauri/Cargo.toml | 1 + src-tauri/src/main.rs | 44 ++++++++++++++++ src-tauri/tauri.conf.json | 2 +- src/components/AppSettingsView.tsx | 80 +++++++++++++++++++++-------- 5 files changed, 178 insertions(+), 31 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 1ac3fbd..f34dc90 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -292,6 +292,17 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "auto-launch" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f012b8cc0c850f34117ec8252a44418f2e34a2cf501de89e29b241ae5f79471" +dependencies = [ + "dirs 4.0.0", + "thiserror 1.0.69", + "winreg 0.10.1", +] + [[package]] name = "autocfg" version = "1.5.1" @@ -974,13 +985,33 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys 0.3.7", +] + [[package]] name = "dirs" version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" dependencies = [ - "dirs-sys", + "dirs-sys 0.5.0", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users 0.4.6", + "winapi", ] [[package]] @@ -991,7 +1022,7 @@ checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", "option-ext", - "redox_users", + "redox_users 0.5.2", "windows-sys 0.61.2", ] @@ -1118,7 +1149,7 @@ dependencies = [ "rustc_version", "toml 1.1.2+spec-1.1.0", "vswhom", - "winreg", + "winreg 0.55.0", ] [[package]] @@ -1677,7 +1708,7 @@ dependencies = [ "coreaudio-rs", "cpal", "crossbeam-channel", - "dirs", + "dirs 6.0.0", "enigo", "futures-util", "iana-time-zone", @@ -1693,6 +1724,7 @@ dependencies = [ "serde_json", "tauri", "tauri-build", + "tauri-plugin-autostart", "tauri-plugin-dialog", "tauri-plugin-global-shortcut", "tauri-plugin-log", @@ -3614,6 +3646,17 @@ dependencies = [ "bitflags 2.12.1", ] +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 1.0.69", +] + [[package]] name = "redox_users" version = "0.5.2" @@ -4595,7 +4638,7 @@ dependencies = [ "anyhow", "bytes", "cookie", - "dirs", + "dirs 6.0.0", "dunce", "embed_plist", "getrandom 0.3.4", @@ -4646,7 +4689,7 @@ checksum = "4aa1f9055fc23919a54e4e125052bed16ed04aef0487086e758fe01a67b451c7" dependencies = [ "anyhow", "cargo_toml", - "dirs", + "dirs 6.0.0", "glob", "heck 0.5.0", "json-patch", @@ -4716,6 +4759,20 @@ dependencies = [ "walkdir", ] +[[package]] +name = "tauri-plugin-autostart" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "459383cebc193cdd03d1ba4acc40f2c408a7abce419d64bdcd2d745bc2886f70" +dependencies = [ + "auto-launch", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", +] + [[package]] name = "tauri-plugin-dialog" version = "2.7.1" @@ -5338,7 +5395,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15edbb0d80583e85ee8df283410038e17314df5cba30da2087a54a85216c0773" dependencies = [ "crossbeam-channel", - "dirs", + "dirs 6.0.0", "libappindicator", "muda", "objc2 0.6.4", @@ -6351,6 +6408,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] + [[package]] name = "winreg" version = "0.55.0" @@ -6471,7 +6537,7 @@ dependencies = [ "block2 0.6.2", "cookie", "crossbeam-channel", - "dirs", + "dirs 6.0.0", "dom_query", "dpi", "dunce", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 7d3430b..4591a33 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -52,3 +52,4 @@ enigo = "0.3" [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] tauri-plugin-global-shortcut = "2" tauri-plugin-dialog = "2" +tauri-plugin-autostart = "2" diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 728ef28..97e3669 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -8,6 +8,7 @@ use std::time::{Duration, Instant}; use tauri::menu::{Menu, MenuItem}; use tauri::tray::TrayIconBuilder; use tauri::{async_runtime::spawn, Emitter, Manager, State}; +use tauri_plugin_autostart::ManagerExt; use tokio::sync::Mutex as TokioMutex; mod audio; @@ -66,6 +67,9 @@ pub struct AppState { pub region: &'static str, } +/// Passed to the app by the autostart entry so it can stay in the tray. +const HIDDEN_LAUNCH_FLAG: &str = "--hidden"; + const TRAY_ID: &str = "main-tray"; const TRAY_ICON_IDLE: &[u8] = include_bytes!("../icons/32x32.png"); const TRAY_ICON_DICTATING: &[u8] = include_bytes!("../icons/tray-microphone.png"); @@ -874,6 +878,26 @@ async fn minimize_window(window: tauri::Window) -> Result<(), String> { window.minimize().map_err(|e| e.to_string()) } +/// Whether the app is registered to start on login (registry Run key on +/// Windows, LaunchAgent on macOS). +#[tauri::command] +fn get_autostart(app: tauri::AppHandle) -> Result { + app.autolaunch().is_enabled().map_err(|e| e.to_string()) +} + +/// Register or remove the login entry. The entry carries `--hidden`, so an +/// automatic launch goes straight to the tray without showing the window. +#[tauri::command] +fn set_autostart(app: tauri::AppHandle, enabled: bool) -> Result<(), String> { + let manager = app.autolaunch(); + if enabled { + manager.enable() + } else { + manager.disable() + } + .map_err(|e| e.to_string()) +} + #[tauri::command] async fn register_dictation_hotkey( hotkey: String, @@ -1112,6 +1136,10 @@ fn main() { .build(), ) .plugin(tauri_plugin_global_shortcut::Builder::new().build()) + .plugin(tauri_plugin_autostart::init( + tauri_plugin_autostart::MacosLauncher::LaunchAgent, + Some(vec![HIDDEN_LAUNCH_FLAG]), + )) .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_notification::init()) .setup(|app| { @@ -1211,8 +1239,22 @@ fn main() { }) .build(app)?; + // The window starts hidden (see tauri.conf.json) so an autostart + // launch never flashes a window; a manual launch shows it here. + let hidden_launch = std::env::args().any(|arg| arg == HIDDEN_LAUNCH_FLAG); + // Intercept window close: hide instead of destroying if let Some(window) = app.get_webview_window("main") { + if hidden_launch { + #[cfg(target_os = "macos")] + let _ = window + .app_handle() + .set_activation_policy(tauri::ActivationPolicy::Accessory); + } else { + let _ = window.show(); + let _ = window.set_focus(); + } + let w = window.clone(); window.on_window_event(move |event| { if let tauri::WindowEvent::CloseRequested { api, .. } = event { @@ -1256,6 +1298,8 @@ fn main() { show_window, hide_window, minimize_window, + get_autostart, + set_autostart, config::save_hotkey, config::get_hotkey, config::save_language_settings, diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 84abb92..7aacd50 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -18,7 +18,7 @@ "height": 600, "resizable": false, "fullscreen": false, - "visible": true + "visible": false } ] }, diff --git a/src/components/AppSettingsView.tsx b/src/components/AppSettingsView.tsx index c498056..5c08d7f 100644 --- a/src/components/AppSettingsView.tsx +++ b/src/components/AppSettingsView.tsx @@ -1,3 +1,5 @@ +import { useEffect, useState } from "react"; +import { invoke } from "@tauri-apps/api/core"; import type { AppSettings, AudioDeviceInfo } from "../types"; import { formatKeySymbol, @@ -67,6 +69,19 @@ export function AppSettingsView({ onDone: () => void; onOpenLogs: () => void; }) { + const [autostart, setAutostart] = useState(false); + + useEffect(() => { + invoke("get_autostart") + .then(setAutostart) + .catch(() => {}); + }, []); + + const toggleAutostart = async (enabled: boolean) => { + await invoke("set_autostart", { enabled }); + setAutostart(enabled); + }; + const renderKbdKeys = (keys: string[]) => sortKeys(keys).map((key, i) => ( @@ -214,28 +229,49 @@ export function AppSettingsView({ -
- - +
+
+ + +
+ +
+ + +