From 42418f9a63cbfab714cb13d3f7ca12b8da146e2f Mon Sep 17 00:00:00 2001 From: George Date: Sun, 22 Mar 2026 00:19:00 +0400 Subject: [PATCH] Add windows option to launch app on startup --- Cargo.lock | 11 ++++ Cargo.toml | 1 + src/status_tray_not_linux.rs | 124 ++++++++++++++++++++++++++++++++++- 3 files changed, 135 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 4112f9a..43785d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1343,6 +1343,7 @@ dependencies = [ "tray-icon", "windows 0.62.2", "winit", + "winreg", ] [[package]] @@ -3870,6 +3871,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if 1.0.4", + "windows-sys 0.59.0", +] + [[package]] name = "wit-bindgen" version = "0.51.0" diff --git a/Cargo.toml b/Cargo.toml index 70ba31a..00a1bf5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ winit = "0.30.13" [target.'cfg(target_os = "windows")'.dependencies] image = "0.25.10" +winreg = "0.55.0" windows = { version = "0.62.2", features = [ "Win32_System_LibraryLoader", ] } diff --git a/src/status_tray_not_linux.rs b/src/status_tray_not_linux.rs index ba2d688..d14c224 100644 --- a/src/status_tray_not_linux.rs +++ b/src/status_tray_not_linux.rs @@ -5,13 +5,25 @@ use std::{ use hyper_headset::devices::{DeviceEvent, DeviceProperties, PropertyType}; use tray_icon::{ - menu::{Menu, MenuEvent, MenuId, MenuItem, PredefinedMenuItem, Submenu}, + menu::{CheckMenuItem, Menu, MenuEvent, MenuId, MenuItem, PredefinedMenuItem, Submenu}, TrayIcon, TrayIconBuilder, }; use winit::{application::ApplicationHandler, event::StartCause}; +#[cfg(target_os = "windows")] +use winreg::{ + enums::{RegType, HKEY_CURRENT_USER, KEY_READ, KEY_SET_VALUE}, + RegKey, RegValue, +}; const NO_COMPATIBLE_DEVICE: &str = "No compatible device found. Is the dongle plugged in?"; const HEADSET_NOT_CONNECTED: &str = "Headset is not connected"; +#[cfg(target_os = "windows")] +const RUN_KEY_PATH: &str = r"Software\Microsoft\Windows\CurrentVersion\Run"; +#[cfg(target_os = "windows")] +const STARTUP_APPROVED_RUN_KEY_PATH: &str = + r"Software\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\Run"; +#[cfg(target_os = "windows")] +const STARTUP_VALUE_NAME: &str = "HyperHeadset"; #[cfg(target_os = "windows")] fn create_tray_icon() -> tray_icon::Icon { @@ -136,6 +148,7 @@ impl TrayApp { #[cfg(target_os = "windows")] { + append_startup_toggle(&menu, &mut new_callbacks); menu.append(&quit_item).unwrap(); new_callbacks.insert(quit_item.id().clone(), Box::new(|| std::process::exit(0))); } @@ -160,6 +173,7 @@ impl TrayApp { #[cfg(target_os = "windows")] { + append_startup_toggle(&menu, &mut new_callbacks); menu.append(&quit_item).unwrap(); new_callbacks.insert(quit_item.id().clone(), Box::new(|| std::process::exit(0))); } @@ -283,6 +297,7 @@ impl TrayApp { #[cfg(target_os = "windows")] { + append_startup_toggle(&menu, &mut new_callbacks); menu.append(&quit_item).unwrap(); new_callbacks.insert(quit_item.id().clone(), Box::new(|| std::process::exit(0))); } @@ -297,6 +312,113 @@ impl TrayApp { } } +#[cfg(target_os = "windows")] +fn append_startup_toggle( + menu: &Menu, + callbacks: &mut HashMap>, +) { + let startup_enabled = is_start_with_windows_enabled(); + let startup_item = CheckMenuItem::new("Start with Windows", true, startup_enabled, None); + let _ = menu.append(&startup_item); + callbacks.insert( + startup_item.id().clone(), + Box::new(|| { + let currently_enabled = is_start_with_windows_enabled(); + if let Err(error) = set_start_with_windows_enabled(!currently_enabled) { + eprintln!("Failed to update startup setting: {error}"); + } + }), + ); +} + +#[cfg(target_os = "windows")] +fn startup_command_line() -> std::io::Result { + let exe_path = std::env::current_exe()?; + Ok(format!("\"{}\"", exe_path.display())) +} + +#[cfg(target_os = "windows")] +fn open_run_key_with_access(access: u32) -> std::io::Result { + RegKey::predef(HKEY_CURRENT_USER).open_subkey_with_flags(RUN_KEY_PATH, access) +} + +#[cfg(target_os = "windows")] +fn open_or_create_run_key_with_access(access: u32) -> std::io::Result { + let hkcu = RegKey::predef(HKEY_CURRENT_USER); + let (run_key, _) = hkcu.create_subkey_with_flags(RUN_KEY_PATH, access)?; + Ok(run_key) +} + +#[cfg(target_os = "windows")] +fn open_startup_approved_key_with_access(access: u32) -> std::io::Result { + RegKey::predef(HKEY_CURRENT_USER).open_subkey_with_flags(STARTUP_APPROVED_RUN_KEY_PATH, access) +} + +#[cfg(target_os = "windows")] +fn open_or_create_startup_approved_key_with_access(access: u32) -> std::io::Result { + let hkcu = RegKey::predef(HKEY_CURRENT_USER); + let (key, _) = hkcu.create_subkey_with_flags(STARTUP_APPROVED_RUN_KEY_PATH, access)?; + Ok(key) +} + +#[cfg(target_os = "windows")] +fn startup_approved_state() -> Option { + let Ok(key) = open_startup_approved_key_with_access(KEY_READ) else { + return None; + }; + let Ok(value) = key.get_raw_value(STARTUP_VALUE_NAME) else { + return None; + }; + match value.bytes.first().copied() { + Some(0x02) => Some(true), + Some(0x03) => Some(false), + _ => None, + } +} + +#[cfg(target_os = "windows")] +fn set_startup_approved_state(enabled: bool) -> std::io::Result<()> { + let key = open_or_create_startup_approved_key_with_access(KEY_SET_VALUE)?; + // 0x02 => enabled, 0x03 => disabled (same convention used by Startup Apps) + let state = if enabled { 0x02u8 } else { 0x03u8 }; + key.set_raw_value( + STARTUP_VALUE_NAME, + &RegValue { + vtype: RegType::REG_BINARY, + bytes: vec![state, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + }, + )?; + Ok(()) +} + +#[cfg(target_os = "windows")] +fn is_start_with_windows_enabled() -> bool { + let Ok(run_key) = open_run_key_with_access(KEY_READ) else { + return false; + }; + if run_key.get_value::(STARTUP_VALUE_NAME).is_err() { + return false; + } + + startup_approved_state().unwrap_or(true) +} + +#[cfg(target_os = "windows")] +fn set_start_with_windows_enabled(enabled: bool) -> std::io::Result<()> { + let run_key = open_or_create_run_key_with_access(KEY_SET_VALUE)?; + if enabled { + run_key.set_value(STARTUP_VALUE_NAME, &startup_command_line()?)?; + set_startup_approved_state(true)?; + } else { + // Keep the Run entry so Windows Startup Apps can manage the toggle too. + if run_key.get_value::(STARTUP_VALUE_NAME).is_err() { + run_key.set_value(STARTUP_VALUE_NAME, &startup_command_line()?)?; + } + set_startup_approved_state(false)?; + } + Ok(()) +} + #[cfg(target_os = "windows")] /// Dark magic to set dark mode unsafe fn enable_dark_context_menus() {