From 67253d02f8e8726a76ff331e8254fce8df88ce45 Mon Sep 17 00:00:00 2001 From: Lotus CI Date: Wed, 23 Sep 2026 22:46:24 -0400 Subject: [PATCH] feat(autostart): start minimized to the tray on login (#3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Launch-on-login now starts the app with `--autostart`. With "Start minimized" on (the default), that launch begins "settled", so neither the page-load reveal nor the 8 s failsafe shows the window: it stays in the tray with sync and notifications running. The tray, a second launch or a deep link reveals it. - The choice is a file in the app config dir (`start-minimized`), read before the web client loads; `get_start_minimized` / `set_start_minimized` back the new Settings switch. - Existing registrations made without args are rewritten at startup (re-enable), so current launch-on-login users get `--autostart` too. - window-state no longer restores VISIBLE: it show()ed the window at creation whenever it was last closed visible (or on first run), before the page painted and even on a login start. Visibility is owned by the page-load reveal + failsafe. Verified on Linux under Xvfb with the debug build: normal launch → shown; --autostart → hidden (past the failsafe) and still running; --autostart with the setting off → shown; --autostart after quitting visible → hidden; normal launch after quitting visible → shown; a second launch reveals the hidden instance; an existing autostart entry gets `--autostart` added. Bump cinny (the Settings switch and the SW protocol guard). Closes #3 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA --- cinny | 2 +- src-tauri/src/lib.rs | 72 +++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 69 insertions(+), 5 deletions(-) diff --git a/cinny b/cinny index 146ba4d..4a98238 160000 --- a/cinny +++ b/cinny @@ -1 +1 @@ -Subproject commit 146ba4d2ffb0ded4119af34736433a0eb89597f9 +Subproject commit 4a9823890baaa6e285d44aeace3eace117f59cd7 diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 99bbb12..ca40c5a 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -582,6 +582,42 @@ fn get_tray_dnd(app: tauri::AppHandle) -> bool { .unwrap_or(false) } +/// cinny-desktop #3: launch-on-login starts the app with this arg, so a login +/// start can stay in the tray instead of opening the window. +const AUTOSTART_ARG: &str = "--autostart"; + +/// Where the "start minimized when launched at login" choice lives. A plain file +/// in the app config dir, because it is needed natively before the web client +/// (and its settings) has loaded. +fn start_minimized_file(app: &tauri::AppHandle) -> Option { + app.path() + .app_config_dir() + .ok() + .map(|dir| dir.join("start-minimized")) +} + +/// The saved choice; defaults to on (a login start lands in the tray). +fn read_start_minimized(app: &tauri::AppHandle) -> bool { + start_minimized_file(app) + .and_then(|path| std::fs::read_to_string(path).ok()) + .map(|value| value.trim() != "0") + .unwrap_or(true) +} + +#[tauri::command] +fn get_start_minimized(app: tauri::AppHandle) -> bool { + read_start_minimized(&app) +} + +#[tauri::command] +fn set_start_minimized(app: tauri::AppHandle, value: bool) -> Result<(), String> { + let path = start_minimized_file(&app).ok_or("no app config dir")?; + if let Some(dir) = path.parent() { + std::fs::create_dir_all(dir).map_err(|e| e.to_string())?; + } + std::fs::write(path, if value { "1" } else { "0" }).map_err(|e| e.to_string()) +} + /// Paint a small white-ringed dot of `color` into the bottom-right (or, with /// `left`, bottom-left) corner of an RGBA buffer, in place. Cross-platform /// (operates on raw pixels). @@ -779,6 +815,8 @@ pub fn run() { set_tray_update_ready, set_tray_call_state, set_taskbar_progress, + get_start_minimized, + set_start_minimized, flash_window, focus_main_window, send_notification, @@ -805,10 +843,16 @@ pub fn run() { // decorated=false at startup would re-create the frameless window // BEFORE lib.rs applies Mica (a broken combination) and before the web // side has pushed the user's current setting. + // VISIBLE is excluded too: the plugin `show()`s the window as soon as + // it is created whenever the saved state was visible (or nothing was + // saved yet) — before the page paints, and even on a launch-on-login + // start that should stay in the tray (cinny-desktop #3). Visibility is + // owned by the page-load reveal + failsafe in `setup`. tauri_plugin_window_state::Builder::default() .with_state_flags( tauri_plugin_window_state::StateFlags::all() - & !tauri_plugin_window_state::StateFlags::DECORATIONS, + & !tauri_plugin_window_state::StateFlags::DECORATIONS + & !tauri_plugin_window_state::StateFlags::VISIBLE, ) .build(), ) @@ -822,10 +866,11 @@ pub fn run() { // P6-1 launch-on-login. The web side drives it via the plugin's own // `plugin:autostart|enable`/`disable`/`is-enabled` commands (no wrapper // command). The MacosLauncher arg is mandatory by the plugin API even - // though macOS is out of scope; `None` = no extra launch args. + // though macOS is out of scope. `--autostart` marks a login start + // (cinny-desktop #3). builder = builder.plugin(tauri_plugin_autostart::init( tauri_plugin_autostart::MacosLauncher::LaunchAgent, - None, + Some(vec![AUTOSTART_ARG]), )); } @@ -938,7 +983,15 @@ pub fn run() { // set true by the on_page_load reveal (window shown) and by the // close-to-tray handler (window intentionally hidden). The 8s failsafe // below only reveals the window if neither of those has happened. - let window_settled = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)); + // + // cinny-desktop #3: a launch-on-login start with "start minimized" on + // begins settled, so neither the reveal nor the failsafe shows the + // window — it stays in the tray (sync + notifications running) until + // the tray icon, a second launch or a deep link reveals it. + let hide_on_launch = std::env::args().any(|arg| arg == AUTOSTART_ARG) + && read_start_minimized(app.handle()); + let window_settled = + std::sync::Arc::new(std::sync::atomic::AtomicBool::new(hide_on_launch)); let settled_page_load = window_settled.clone(); let window = WebviewWindowBuilder::new(app, "main".to_string(), window_url) .title("Lotus Chat") @@ -1013,6 +1066,17 @@ pub fn run() { } }); + // Registrations made before the `--autostart` arg existed have no + // args; re-enabling rewrites the OS entry with the current ones. + #[cfg(desktop)] + { + use tauri_plugin_autostart::ManagerExt; + let autolaunch = app.autolaunch(); + if autolaunch.is_enabled().unwrap_or(false) { + let _ = autolaunch.enable(); + } + } + // Deep links (matrix:): route both the cold-start case and the // already-running case (forwarded via single-instance argv) into the // web client.