Anki/qt/launcher/src/platform/windows.rs
Damien Elmes aeaf001df7 Hack back in a fix for lack of ANSI codes on Windows 10
There must be a better way to do this, but someone more familiar
with Win32 internals than I will need to discover it.

https://forums.ankiweb.net/t/anki-25-08-beta/63645/61
2025-07-19 00:40:16 +07:00

243 lines
7.4 KiB
Rust

// 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::<OSVERSIONINFOW>() 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<std::path::PathBuf> {
// 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<std::path::PathBuf> {
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<u16> = 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();
}