mirror of
https://github.com/ankitects/anki.git
synced 2025-09-21 23:42:23 -04:00

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
243 lines
7.4 KiB
Rust
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();
|
|
}
|