// Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html use std::io::stdin; use std::process::Command; use anyhow::Context; use anyhow::Result; use widestring::u16cstr; use windows::core::PCWSTR; use windows::Wdk::System::SystemServices::RtlGetVersion; use windows::Win32::System::Console::AttachConsole; use windows::Win32::System::Console::GetConsoleWindow; use windows::Win32::System::Console::ATTACH_PARENT_PROCESS; use windows::Win32::System::Registry::RegCloseKey; use windows::Win32::System::Registry::RegOpenKeyExW; use windows::Win32::System::Registry::RegQueryValueExW; use windows::Win32::System::Registry::HKEY; use windows::Win32::System::Registry::HKEY_CURRENT_USER; use windows::Win32::System::Registry::KEY_READ; use windows::Win32::System::Registry::REG_SZ; use windows::Win32::System::SystemInformation::OSVERSIONINFOW; use windows::Win32::UI::Shell::SetCurrentProcessExplicitAppUserModelID; /// Returns true if running on Windows 10 (not Windows 11) fn is_windows_10() -> bool { unsafe { let mut info = OSVERSIONINFOW { dwOSVersionInfoSize: std::mem::size_of::() as u32, ..Default::default() }; if RtlGetVersion(&mut info).is_ok() { // Windows 10 has build numbers < 22000, Windows 11 >= 22000 info.dwBuildNumber < 22000 && info.dwMajorVersion == 10 } else { false } } } pub fn ensure_terminal_shown() -> Result<()> { unsafe { if !GetConsoleWindow().is_invalid() { // We already have a console, no need to spawn anki-console.exe return Ok(()); } } if std::env::var("ANKI_IMPLICIT_CONSOLE").is_ok() && attach_to_parent_console() { // This black magic triggers Windows to switch to the new // ANSI-supporting console host, which is usually only available // when the app is built with the console subsystem. // Only needed on Windows 10, not Windows 11. if is_windows_10() { let _ = Command::new("cmd").args(["/C", ""]).status(); } // Successfully attached to parent console reconnect_stdio_to_console(); return Ok(()); } // No console available, spawn anki-console.exe and exit let current_exe = std::env::current_exe().context("Failed to get current executable path")?; let exe_dir = current_exe .parent() .context("Failed to get executable directory")?; let console_exe = exe_dir.join("anki-console.exe"); if !console_exe.exists() { anyhow::bail!("anki-console.exe not found in the same directory"); } // Spawn anki-console.exe without waiting Command::new(&console_exe) .env("ANKI_IMPLICIT_CONSOLE", "1") .spawn() .context("Failed to spawn anki-console.exe")?; // Exit immediately after spawning std::process::exit(0); } pub fn attach_to_parent_console() -> bool { unsafe { if !GetConsoleWindow().is_invalid() { // we have a console already return false; } if AttachConsole(ATTACH_PARENT_PROCESS).is_ok() { // successfully attached to parent reconnect_stdio_to_console(); true } else { false } } } /// Reconnect stdin/stdout/stderr to the console. fn reconnect_stdio_to_console() { use std::ffi::CString; use libc_stdhandle::*; // we launched without a console, so we'll need to open stdin/out/err let conin = CString::new("CONIN$").unwrap(); let conout = CString::new("CONOUT$").unwrap(); let r = CString::new("r").unwrap(); let w = CString::new("w").unwrap(); // Python uses the CRT for I/O, and it requires the descriptors are reopened. unsafe { libc::freopen(conin.as_ptr(), r.as_ptr(), stdin()); libc::freopen(conout.as_ptr(), w.as_ptr(), stdout()); libc::freopen(conout.as_ptr(), w.as_ptr(), stderr()); } } pub fn finalize_uninstall() { let uninstaller_path = get_uninstaller_path(); match uninstaller_path { Some(path) => { println!("Launching Windows uninstaller..."); let result = Command::new(&path).env("ANKI_LAUNCHER", "1").spawn(); match result { Ok(_) => { println!("Uninstaller launched successfully."); return; } Err(e) => { println!("Failed to launch uninstaller: {e}"); println!("You can manually run: {}", path.display()); } } } None => { println!("Windows uninstaller not found."); println!("You may need to uninstall via Windows Settings > Apps."); } } println!("Press enter to close..."); let mut input = String::new(); let _ = stdin().read_line(&mut input); } fn get_uninstaller_path() -> Option { // Try to read install directory from registry if let Some(install_dir) = read_registry_install_dir() { let uninstaller = install_dir.join("uninstall.exe"); if uninstaller.exists() { return Some(uninstaller); } } // Fall back to default location let default_dir = dirs::data_local_dir()?.join("Programs").join("Anki"); let uninstaller = default_dir.join("uninstall.exe"); if uninstaller.exists() { return Some(uninstaller); } None } fn read_registry_install_dir() -> Option { unsafe { let mut hkey = HKEY::default(); // Convert the registry path to wide string let subkey = u16cstr!("SOFTWARE\\Anki"); // Open the registry key let result = RegOpenKeyExW( HKEY_CURRENT_USER, PCWSTR(subkey.as_ptr()), Some(0), KEY_READ, &mut hkey, ); if result.is_err() { return None; } // Query the Install_Dir64 value let value_name = u16cstr!("Install_Dir64"); let mut value_type = REG_SZ; let mut data_size = 0u32; // First call to get the size let result = RegQueryValueExW( hkey, PCWSTR(value_name.as_ptr()), None, Some(&mut value_type), None, Some(&mut data_size), ); if result.is_err() || data_size == 0 { let _ = RegCloseKey(hkey); return None; } // Allocate buffer and read the value let mut buffer: Vec = vec![0; (data_size / 2) as usize]; let result = RegQueryValueExW( hkey, PCWSTR(value_name.as_ptr()), None, Some(&mut value_type), Some(buffer.as_mut_ptr() as *mut u8), Some(&mut data_size), ); let _ = RegCloseKey(hkey); if result.is_ok() { // Convert wide string back to PathBuf let len = buffer.iter().position(|&x| x == 0).unwrap_or(buffer.len()); let path_str = String::from_utf16_lossy(&buffer[..len]); Some(std::path::PathBuf::from(path_str)) } else { None } } } pub fn prepare_to_launch_normally() { // Set the App User Model ID for Windows taskbar grouping unsafe { let _ = SetCurrentProcessExplicitAppUserModelID(PCWSTR(u16cstr!("Ankitects.Anki").as_ptr())); } attach_to_parent_console(); }