Files

14 KiB
Raw Permalink Blame History

Run Warp at startup (Windows) — Tech Spec

Linear: CODE-1786 See PRODUCT.md for user-visible behavior.

Context

The macOS "Start Warp at login" feature is already plumbed end-to-end. This ticket extends that plumbing to Windows by adding a new registration backend and broadening the existing setting/UI's platform gate; no new user-visible surface is being designed. Relevant code today:

  • app/src/terminal/general_settings.rs:36-55add_app_as_login_item (LoginItem) and app_added_as_login_item (AppAddedAsLoginItem), both gated on SupportedPlatforms::MAC with defaults true / false respectively. The second setting is the "already registered" bookkeeping that prevents clobbering a manual unregister.
  • app/src/lib.rs:2229-2239 — startup wiring. Subscribes to GeneralSettingsChangedEvent::LoginItem and calls maybe_register_app_as_login_item on change + once at launch. Entire block is #[cfg(target_os = "macos")].
  • app/src/lib.rs:2279-2362maybe_register_app_as_login_item. macOS-only. Guards on WARP_INTEGRATION, skips when bundle identifier is missing or equals dev.warp.Warp-Local, runs SMAppService register/unregisterAndReturnError: off the UI thread, then writes the result back to app_added_as_login_item.
  • app/src/settings_view/features_page.rs — toggle UI:
    • action enum: FeaturesPageAction::ToggleLoginItem (~l618)
    • telemetry: ToggleLoginItem branch (~l975-978)
    • handler: ToggleLoginItem => ... (~l1855-1857)
    • widget: LoginItemWidget (~l4488-4533), rendered only when add_app_as_login_item.is_supported_on_current_platform() returns true (~l2481-2486). Label is hard-coded to "Start Warp at login (requires macOS 13+)".
  • crates/settings/src/lib.rs:161-219SupportedPlatforms enum and matches_current_platform. Supports OR(…, …) so MAC + WINDOWS can be expressed without adding a new variant.
  • crates/warp_core/src/channel/state.rs:119-125, 40ChannelState::app_id() returns the current channel's AppId (e.g. dev.openwarp.OpenWarp, dev.warp.Warp, dev.warp.WarpPreview, dev.warp.WarpDev). We can reuse application_name() for the channel-specific registry value name on Windows.
  • app/Cargo.toml:356-377 — Windows-only dependency block. winreg, windows-registry, and windows are already pulled in and available for a new module. winreg is already used for reading registry values (crates/warpui/src/windowing/winit/windows/registry.rs), so we should follow that pattern for consistency.

Proposed changes

1. Loosen the setting's platform gate

Change both settings in app/src/terminal/general_settings.rs from SupportedPlatforms::MAC to SupportedPlatforms::OR(Box::new(SupportedPlatforms::MAC), Box::new(SupportedPlatforms::WINDOWS)). Defaults stay the same (true / false). The toml_path / description stay unchanged since the TOML key is already platform-neutral (general.login_item). No migration is required: for existing Windows users, the setting simply starts populating with its default value the next time preferences are loaded.

2. Add a Windows registration backend

Create app/src/login_item/mod.rs (or reuse an existing "platform adapters" spot — see "Module placement" below) to own the cross-platform entry point, replacing the current macOS-only free function.

// app/src/login_item/mod.rs
pub fn maybe_register_app_as_login_item(ctx: &mut AppContext) {
    if std::env::var("WARP_INTEGRATION").is_ok() {
        log::debug!("Not registering as a login item in integration tests");
        return;
    }
    #[cfg(target_os = "macos")]
    macos::maybe_register(ctx);
    #[cfg(target_os = "windows")]
    windows::maybe_register(ctx);
}

Move the existing macOS body into login_item/macos.rs unchanged. Add login_item/windows.rs with the Windows implementation:

  • Pull the desired state + the already-registered flag off GeneralSettings the same way the macOS path does.
  • Short-circuit if add_app_as_login_item && app_added_as_login_item (the existing "don't clobber a manual unregister" contract from lib.rs:2290-2296).
  • Resolve the executable path via std::env::current_exe()? then dunce::canonicalize. std::fs::canonicalize on Windows always returns a Win32 verbatim (\\?\) path, which is ugly in Settings → Apps → Startup / Task Manager and trips up some third-party tools that parse the Run value; dunce strips the prefix when safe and leaves it alone for real UNC / long paths. Bail with a debug log if resolution fails.
  • Skip registration for dev/local builds. The cleanest check is ChannelState::is_release_bundle() (see crates/warp_core/src/channel/state.rs:77-79); use it as the Windows equivalent of the macOS "bundle identifier missing / dev.warp.Warp-Local" guard. Explicitly keep the behavior that enabling the toggle in a dev build persists the preference but does not write the registry value, since the preference is synced via user settings and would bleed into release runs otherwise — we only skip the I/O, not the preference update. (Matches macOS: toggling in a non-bundled build is a no-op on the system side.)
  • Off-thread the registry I/O via ctx.spawn, mirroring the macOS path and the existing async signature |settings, app_added_as_login_item, ctx| { ... }. Return bool for "registered successfully" so the existing completion handler can set app_added_as_login_item.
  • Registry work itself:
    use winreg::enums::HKEY_CURRENT_USER;
    use winreg::RegKey;
    const RUN_SUBKEY: &str =
        r"Software\Microsoft\Windows\CurrentVersion\Run";
    fn value_name() -> String {
        // e.g. "Warp", "WarpPreview", "WarpDev", "OpenWarp"
        ChannelState::app_id().application_name().to_owned()
    }
    fn register(exe: &Path) -> std::io::Result<()> {
        let hkcu = RegKey::predef(HKEY_CURRENT_USER);
        let (run_key, _) = hkcu.create_subkey(RUN_SUBKEY)?;
        // Quote the path so spaces (e.g. "C:\Program Files\Warp\warp.exe") survive parsing.
        let value = format!("\"{}\"", exe.display());
        run_key.set_value(&value_name(), &value)
    }
    fn unregister() -> std::io::Result<()> {
        let hkcu = RegKey::predef(HKEY_CURRENT_USER);
        let run_key = hkcu.open_subkey_with_flags(
            RUN_SUBKEY,
            winreg::enums::KEY_SET_VALUE,
        )?;
        match run_key.delete_value(value_name()) {
            Ok(()) => Ok(()),
            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
            Err(e) => Err(e),
        }
    }
    
    HKCU\Software\Microsoft\Windows\CurrentVersion\Run is the user-scope per-login key; it does not require admin, and is what Windows 10/11's Settings → Apps → Startup and Task Manager → Startup apps surface. This satisfies Behavior invariants 34, 7, 11.
  • The per-channel value name from application_name() keeps Dev/Preview/Stable isolated (Behavior 7): Warp, WarpPreview, WarpDev, OpenWarp.
  • Behavior 10 ("moving the install") is partially covered: the short-circuit add_app_as_login_item && app_added_as_login_item intentionally prevents re-registration on every launch, so a moved install keeps the stale path until the user toggles the setting off and back on (which rewrites against the new current_exe()). Automatic detection + rewrite when the stored path differs is tracked as a follow-up.

3. Rewire startup

Replace the existing #[cfg(target_os = "macos")] block in app/src/lib.rs:2229-2239 with a block gated on cfg(any(target_os = "macos", target_os = "windows")), calling the new cross-platform login_item::maybe_register_app_as_login_item. The subscription to GeneralSettingsChangedEvent::LoginItem stays identical. Delete the old maybe_register_app_as_login_item body from lib.rs:2279-2362.

4. Update the Features page UI

Two tiny changes in app/src/settings_view/features_page.rs:

  • LoginItemWidget::render (~l4509): change the hard-coded label to a platform-dependent string, e.g.
    #[cfg(target_os = "macos")]
    let label = "Start Warp at login (requires macOS 13+)";
    #[cfg(target_os = "windows")]
    let label = "Start Warp at login";
    
    Anything else — action enum, telemetry, handler, and widget-registration path — already flows through is_supported_on_current_platform() on the setting itself, so broadening the setting in step 1 automatically turns the toggle on for Windows.
  • Update search_terms for the widget (~l4497) to keep the macOS keyword but add "windows", so the settings search surface finds it on both OSes.

Module placement

We don't have an existing login_item module. A new app/src/login_item/{mod,macos,windows}.rs layout is the smallest, most obvious split and matches other OS-sharded areas (app/src/terminal/local_tty/windows/*, app/src/util/file/external_editor/{mod,windows}.rs, app/src/antivirus/windows.rs, app/src/terminal/audible_bell/{mod,windows}.rs). Add mod login_item; in app/src/lib.rs and re-export maybe_register_app_as_login_item there.

Testing and validation

Verification maps back to the numbered invariants in PRODUCT.md. Unit tests (Windows, gated with #[cfg(target_os = "windows")]):

  • Invariant 3 (enable registers). Drive register() against a temporary HKCU subkey (or wrap the subkey path in a trait and use a fake in tests) and assert the value is "\"<path>\"" under the expected name.
  • Invariant 4 (disable unregisters, idempotent). Call unregister() twice in a row; both must succeed. Calling unregister() when the value was never set must succeed.
  • Invariant 7 (per-channel isolation). With different ChannelState app IDs, value_name() returns distinct strings (Warp, WarpPreview, WarpDev). Registering under one does not touch another.
  • Invariant 10 (path move). Register with path A, then with path B; the stored value is B. No leftover value under a different name. Because the real Software\Microsoft\Windows\CurrentVersion\Run hive is shared user state that should not be mutated by tests, the registry helpers should accept an injectable subkey path (e.g. register_with_subkey(subkey, …)) and the tests should drive them against Software\Warp\TestRun\<uuid> under HKCU, cleaning up on drop. Follow the crates/warpui/src/windowing/winit/windows/registry.rs style for the read-side helper for consistency. Cross-platform tests (run on every OS):
  • Invariant 1/13 (platform gate). Assert LoginItem::supported_platforms().matches_current_platform() is true on mac+windows, false on wasm. This is already covered structurally by SupportedPlatforms::OR; a small regression test in general_settings/features_page confirms the intent stays stable.
  • Invariant 9 (integration test guard). With WARP_INTEGRATION set, calling maybe_register_app_as_login_item does no registry I/O. Can be asserted by calling with a mock subkey provider that panics if invoked. Manual validation (Windows):
  1. Invariants 1, 2, 11. Install a release-bundle Windows build, verify Features → General shows Start Warp at login, toggle default is on, and HKCU\Software\Microsoft\Windows\CurrentVersion\Run\Warp exists with the installed exe path. Confirm the entry is visible in Settings → Apps → Startup and Task Manager → Startup apps with the display name "Warp".
  2. Invariant 34. Toggle off; confirm the registry value is gone and Windows UIs no longer show the entry. Toggle back on; confirm value is rewritten.
  3. Invariant 5. With toggle on, delete the registry value (or disable from Task Manager). Relaunch Warp; verify the value is not recreated and app_added_as_login_item stays true. Toggle off then on in-app; verify the value is recreated.
  4. Invariant 6. Sign out and back into Windows. Warp launches without stealing focus; the global hotkey surfaces it as expected.
  5. Invariant 7. Install both Stable and Preview channels; enable the setting in each; confirm both registry values (Warp, WarpPreview) coexist and unregistering one doesn't affect the other.
  6. Invariant 8. Run cargo run from a dev checkout with the toggle on; confirm no registry value is written and the preference updates still persist.
  7. Invariant 10. Move/rename the install directory, relaunch Warp, toggle off+on; confirm the registry value points at the new path. Telemetry (Invariant 12): filter the dashboard for FeaturesPageAction { action: "ToggleLoginItem" } and confirm Windows events arrive alongside macOS ones post-rollout.

Risks and mitigations

  • Silently re-enabling a user's manual removal. Mitigated by reusing the existing app_added_as_login_item bookkeeping contract — Windows code must respect it the same way macOS does. Covered by Invariant 5 and the relevant manual test.
  • Path quoting bugs. Windows start entries are fragile with paths containing spaces. Store the value as a single quoted string, and add a regression test for a path with spaces.
  • Verbatim path prefix. std::fs::canonicalize returns \\?\-prefixed paths on Windows, which render oddly in Settings → Apps → Startup and confuse some third-party launchers. Use dunce::canonicalize so the stored Run value is a plain drive-letter path for the common case, while still tolerating real UNC / long paths.
  • Shared registry key across tests/processes. Use injectable subkey paths in unit tests so the real Run key is never touched by CI.
  • Dev-build regressions. Gate actual registry I/O on ChannelState::is_release_bundle() to avoid developer machines auto-launching random target/debug/warp.exe artifacts.

Follow-ups

  • Linux autostart (~/.config/autostart/warp.desktop) is a natural next step with the same setting and the same UX, but is out of scope for this ticket.
  • Optional future polish: automatically rewrite the stored path when current_exe() differs from the registered value, so users never need to re-toggle after a move.