diff --git a/Cargo.lock b/Cargo.lock index 4112f9a..4787538 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.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10" +dependencies = [ + "cfg-if 1.0.4", + "windows-sys 0.61.2", +] + [[package]] name = "wit-bindgen" version = "0.51.0" diff --git a/Cargo.toml b/Cargo.toml index 70ba31a..41c3dd8 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.56.0" windows = { version = "0.62.2", features = [ "Win32_System_LibraryLoader", ] } diff --git a/src/main.rs b/src/main.rs index 5cfbbb4..dae13aa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,5 @@ +#![cfg_attr(target_os = "windows", windows_subsystem = "windows")] + #[cfg(target_os = "linux")] mod status_tray; diff --git a/src/status_tray_not_linux.rs b/src/status_tray_not_linux.rs index 789d435..b5f1e8a 100644 --- a/src/status_tray_not_linux.rs +++ b/src/status_tray_not_linux.rs @@ -7,10 +7,15 @@ use std::{ use image::{Rgba, RgbaImage}; 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, +}; #[cfg(target_os = "windows")] use crate::tray_battery_icon_state::{TrayBatteryIconState, WindowsIconKey}; @@ -18,6 +23,12 @@ use crate::tray_battery_icon_state::{TrayBatteryIconState, WindowsIconKey}; 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"; const WINDOWS_ICON_SIZE: u32 = 16; #[cfg(target_os = "windows")] @@ -320,6 +331,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))); } @@ -344,6 +356,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))); } @@ -467,6 +480,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))); } @@ -481,6 +495,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].into(), + }, + )?; + 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() {