14 KiB
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-55—add_app_as_login_item(LoginItem) andapp_added_as_login_item(AppAddedAsLoginItem), both gated onSupportedPlatforms::MACwith defaultstrue/falserespectively. The second setting is the "already registered" bookkeeping that prevents clobbering a manual unregister.app/src/lib.rs:2229-2239— startup wiring. Subscribes toGeneralSettingsChangedEvent::LoginItemand callsmaybe_register_app_as_login_itemon change + once at launch. Entire block is#[cfg(target_os = "macos")].app/src/lib.rs:2279-2362—maybe_register_app_as_login_item. macOS-only. Guards onWARP_INTEGRATION, skips when bundle identifier is missing or equalsdev.warp.Warp-Local, runsSMAppService register/unregisterAndReturnError:off the UI thread, then writes the result back toapp_added_as_login_item.app/src/settings_view/features_page.rs— toggle UI:- action enum:
FeaturesPageAction::ToggleLoginItem(~l618) - telemetry:
ToggleLoginItembranch (~l975-978) - handler:
ToggleLoginItem => ...(~l1855-1857) - widget:
LoginItemWidget(~l4488-4533), rendered only whenadd_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+)".
- action enum:
crates/settings/src/lib.rs:161-219—SupportedPlatformsenum andmatches_current_platform. SupportsOR(…, …)soMAC+WINDOWScan be expressed without adding a new variant.crates/warp_core/src/channel/state.rs:119-125, 40—ChannelState::app_id()returns the current channel'sAppId(e.g.dev.openwarp.OpenWarp,dev.warp.Warp,dev.warp.WarpPreview,dev.warp.WarpDev). We can reuseapplication_name()for the channel-specific registry value name on Windows.app/Cargo.toml:356-377— Windows-only dependency block.winreg,windows-registry, andwindowsare already pulled in and available for a new module.winregis 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
GeneralSettingsthe 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 fromlib.rs:2290-2296). - Resolve the executable path via
std::env::current_exe()?thendunce::canonicalize.std::fs::canonicalizeon 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 theRunvalue;duncestrips 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()(seecrates/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| { ... }. Returnboolfor "registered successfully" so the existing completion handler can setapp_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\Runis 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 3–4, 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_itemintentionally 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 newcurrent_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.Anything else — action enum, telemetry, handler, and widget-registration path — already flows through#[cfg(target_os = "macos")] let label = "Start Warp at login (requires macOS 13+)"; #[cfg(target_os = "windows")] let label = "Start Warp at login";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_termsfor 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 temporaryHKCUsubkey (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. Callingunregister()when the value was never set must succeed. - Invariant 7 (per-channel isolation). With different
ChannelStateapp 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\Runhive 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 againstSoftware\Warp\TestRun\<uuid>under HKCU, cleaning up on drop. Follow thecrates/warpui/src/windowing/winit/windows/registry.rsstyle 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 bySupportedPlatforms::OR; a small regression test ingeneral_settings/features_pageconfirms the intent stays stable. - Invariant 9 (integration test guard). With
WARP_INTEGRATIONset, callingmaybe_register_app_as_login_itemdoes no registry I/O. Can be asserted by calling with a mock subkey provider that panics if invoked. Manual validation (Windows):
- 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\Warpexists with the installed exe path. Confirm the entry is visible in Settings → Apps → Startup and Task Manager → Startup apps with the display name "Warp". - Invariant 3–4. Toggle off; confirm the registry value is gone and Windows UIs no longer show the entry. Toggle back on; confirm value is rewritten.
- 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_itemstays true. Toggle off then on in-app; verify the value is recreated. - Invariant 6. Sign out and back into Windows. Warp launches without stealing focus; the global hotkey surfaces it as expected.
- 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. - Invariant 8. Run
cargo runfrom a dev checkout with the toggle on; confirm no registry value is written and the preference updates still persist. - 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_itembookkeeping 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::canonicalizereturns\\?\-prefixed paths on Windows, which render oddly in Settings → Apps → Startup and confuse some third-party launchers. Usedunce::canonicalizeso the storedRunvalue 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
Runkey is never touched by CI. - Dev-build regressions. Gate actual registry I/O on
ChannelState::is_release_bundle()to avoid developer machines auto-launching randomtarget/debug/warp.exeartifacts.
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.