mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 14:02:21 -04:00
Add uninstall support to launcher
This commit is contained in:
parent
f89ab00236
commit
731e7d5b5c
7 changed files with 266 additions and 8 deletions
|
@ -138,7 +138,7 @@ unic-ucd-category = "0.9.0"
|
|||
unicode-normalization = "0.1.24"
|
||||
walkdir = "2.5.0"
|
||||
which = "8.0.0"
|
||||
winapi = { version = "0.3", features = ["wincon"] }
|
||||
winapi = { version = "0.3", features = ["wincon", "winreg"] }
|
||||
windows = { version = "0.61.3", features = ["Media_SpeechSynthesis", "Media_Core", "Foundation_Collections", "Storage_Streams"] }
|
||||
wiremock = "0.6.3"
|
||||
xz2 = "0.1.7"
|
||||
|
|
|
@ -37,6 +37,7 @@ struct State {
|
|||
uv_install_root: std::path::PathBuf,
|
||||
uv_cache_dir: std::path::PathBuf,
|
||||
no_cache_marker: std::path::PathBuf,
|
||||
anki_base_folder: std::path::PathBuf,
|
||||
uv_path: std::path::PathBuf,
|
||||
user_pyproject_path: std::path::PathBuf,
|
||||
user_python_version_path: std::path::PathBuf,
|
||||
|
@ -59,6 +60,7 @@ pub enum MainMenuChoice {
|
|||
Version(VersionKind),
|
||||
ToggleBetas,
|
||||
ToggleCache,
|
||||
Uninstall,
|
||||
Quit,
|
||||
}
|
||||
|
||||
|
@ -86,6 +88,7 @@ fn run() -> Result<()> {
|
|||
uv_install_root: uv_install_root.clone(),
|
||||
uv_cache_dir: uv_install_root.join("cache"),
|
||||
no_cache_marker: uv_install_root.join("nocache"),
|
||||
anki_base_folder: get_anki_base_path()?,
|
||||
uv_path: exe_dir.join(get_uv_binary_name()),
|
||||
user_pyproject_path: uv_install_root.join("pyproject.toml"),
|
||||
user_python_version_path: uv_install_root.join(".python-version"),
|
||||
|
@ -95,6 +98,13 @@ fn run() -> Result<()> {
|
|||
sync_complete_marker: uv_install_root.join(".sync_complete"),
|
||||
};
|
||||
|
||||
// Check for uninstall request from Windows uninstaller
|
||||
if std::env::var("ANKI_LAUNCHER_UNINSTALL").is_ok() {
|
||||
ensure_terminal_shown()?;
|
||||
handle_uninstall(&state)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Create install directory and copy project files in
|
||||
create_dir_all(&state.uv_install_root)?;
|
||||
let had_user_pyproj = state.user_pyproject_path.exists();
|
||||
|
@ -200,6 +210,12 @@ fn main_menu_loop(state: &State) -> Result<()> {
|
|||
println!();
|
||||
continue;
|
||||
}
|
||||
MainMenuChoice::Uninstall => {
|
||||
if handle_uninstall(state)? {
|
||||
std::process::exit(0);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
choice @ (MainMenuChoice::Latest | MainMenuChoice::Version(_)) => {
|
||||
// For other choices, update project files and sync
|
||||
update_pyproject_for_version(
|
||||
|
@ -295,7 +311,9 @@ fn get_main_menu_choice(
|
|||
"5) Cache downloads: {}",
|
||||
if cache_enabled { "on" } else { "off" }
|
||||
);
|
||||
println!("6) Quit");
|
||||
println!();
|
||||
println!("6) Uninstall");
|
||||
println!("7) Quit");
|
||||
print!("> ");
|
||||
let _ = stdout().flush();
|
||||
|
||||
|
@ -318,7 +336,8 @@ fn get_main_menu_choice(
|
|||
}
|
||||
"4" => MainMenuChoice::ToggleBetas,
|
||||
"5" => MainMenuChoice::ToggleCache,
|
||||
"6" => MainMenuChoice::Quit,
|
||||
"6" => MainMenuChoice::Uninstall,
|
||||
"7" => MainMenuChoice::Quit,
|
||||
_ => {
|
||||
println!("Invalid input. Please try again.");
|
||||
continue;
|
||||
|
@ -378,6 +397,9 @@ fn update_pyproject_for_version(
|
|||
MainMenuChoice::ToggleCache => {
|
||||
unreachable!();
|
||||
}
|
||||
MainMenuChoice::Uninstall => {
|
||||
unreachable!();
|
||||
}
|
||||
MainMenuChoice::Version(version_kind) => {
|
||||
let content = read_file(&dist_pyproject_path)?;
|
||||
let content_str =
|
||||
|
@ -494,7 +516,7 @@ fn inject_helper_addon(_uv_install_root: &std::path::Path) -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn get_anki_addons21_path() -> Result<std::path::PathBuf> {
|
||||
fn get_anki_base_path() -> Result<std::path::PathBuf> {
|
||||
let anki_base_path = if cfg!(target_os = "windows") {
|
||||
// Windows: %APPDATA%\Anki2
|
||||
dirs::config_dir()
|
||||
|
@ -512,7 +534,61 @@ fn get_anki_addons21_path() -> Result<std::path::PathBuf> {
|
|||
.join("Anki2")
|
||||
};
|
||||
|
||||
Ok(anki_base_path.join("addons21"))
|
||||
Ok(anki_base_path)
|
||||
}
|
||||
|
||||
fn get_anki_addons21_path() -> Result<std::path::PathBuf> {
|
||||
Ok(get_anki_base_path()?.join("addons21"))
|
||||
}
|
||||
|
||||
fn handle_uninstall(state: &State) -> Result<bool> {
|
||||
println!("Uninstall Anki's program files? (y/n)");
|
||||
print!("> ");
|
||||
let _ = stdout().flush();
|
||||
|
||||
let mut input = String::new();
|
||||
let _ = stdin().read_line(&mut input);
|
||||
let input = input.trim().to_lowercase();
|
||||
|
||||
if input != "y" {
|
||||
println!("Uninstall cancelled.");
|
||||
println!();
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// Remove program files
|
||||
if state.uv_install_root.exists() {
|
||||
anki_io::remove_dir_all(&state.uv_install_root)?;
|
||||
println!("Program files removed.");
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("Remove all profiles/cards? (y/n)");
|
||||
print!("> ");
|
||||
let _ = stdout().flush();
|
||||
|
||||
let mut input = String::new();
|
||||
let _ = stdin().read_line(&mut input);
|
||||
let input = input.trim().to_lowercase();
|
||||
|
||||
if input == "y" && state.anki_base_folder.exists() {
|
||||
anki_io::remove_dir_all(&state.anki_base_folder)?;
|
||||
println!("User data removed.");
|
||||
}
|
||||
|
||||
println!();
|
||||
|
||||
// Platform-specific messages
|
||||
#[cfg(target_os = "macos")]
|
||||
platform::mac::finalize_uninstall();
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
platform::windows::finalize_uninstall();
|
||||
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
platform::unix::finalize_uninstall();
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn build_python_command(uv_install_root: &std::path::Path, args: &[String]) -> Result<Command> {
|
||||
|
|
|
@ -67,3 +67,32 @@ pub fn relaunch_in_terminal() -> Result<()> {
|
|||
.ensure_spawn()?;
|
||||
std::process::exit(0);
|
||||
}
|
||||
|
||||
pub fn finalize_uninstall() {
|
||||
if let Ok(exe_path) = std::env::current_exe() {
|
||||
// Find the .app bundle by walking up the directory tree
|
||||
let mut app_bundle_path = exe_path.as_path();
|
||||
while let Some(parent) = app_bundle_path.parent() {
|
||||
if let Some(name) = parent.file_name() {
|
||||
if name.to_string_lossy().ends_with(".app") {
|
||||
let result = Command::new("trash").arg(parent).output();
|
||||
|
||||
match result {
|
||||
Ok(output) if output.status.success() => {
|
||||
println!("Anki has been uninstalled.");
|
||||
return;
|
||||
}
|
||||
_ => {
|
||||
// Fall back to manual instructions
|
||||
println!(
|
||||
"Please manually drag Anki.app to the trash to complete uninstall."
|
||||
);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
app_bundle_path = parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
mod unix;
|
||||
pub mod unix;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub mod mac;
|
||||
|
|
|
@ -47,3 +47,21 @@ pub fn relaunch_in_terminal() -> Result<()> {
|
|||
// If no terminal worked, continue without relaunching
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn finalize_uninstall() {
|
||||
use std::io::stdin;
|
||||
use std::io::stdout;
|
||||
use std::io::Write;
|
||||
|
||||
let uninstall_script = std::path::Path::new("/usr/local/share/anki/uninstall.sh");
|
||||
|
||||
if uninstall_script.exists() {
|
||||
println!("To finish uninstalling, run 'sudo /usr/local/share/anki/uninstall.sh'");
|
||||
} else {
|
||||
println!("Anki has been uninstalled.");
|
||||
}
|
||||
println!("Press enter to quit.");
|
||||
let _ = stdout().flush();
|
||||
let mut input = String::new();
|
||||
let _ = stdin().read_line(&mut input);
|
||||
}
|
||||
|
|
|
@ -1,11 +1,17 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
use std::ffi::OsStr;
|
||||
use std::io::stdin;
|
||||
use std::os::windows::ffi::OsStrExt;
|
||||
use std::process::Command;
|
||||
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use winapi::shared::minwindef::HKEY;
|
||||
use winapi::um::wincon;
|
||||
use winapi::um::winnt::KEY_READ;
|
||||
use winapi::um::winreg;
|
||||
|
||||
pub fn ensure_terminal_shown() -> Result<()> {
|
||||
unsafe {
|
||||
|
@ -79,3 +85,122 @@ fn reconnect_stdio_to_console() {
|
|||
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 = std::ptr::null_mut();
|
||||
|
||||
// Convert the registry path to wide string
|
||||
let subkey: Vec<u16> = OsStr::new("SOFTWARE\\Anki")
|
||||
.encode_wide()
|
||||
.chain(std::iter::once(0))
|
||||
.collect();
|
||||
|
||||
// Open the registry key
|
||||
let result = winreg::RegOpenKeyExW(
|
||||
winreg::HKEY_CURRENT_USER,
|
||||
subkey.as_ptr(),
|
||||
0,
|
||||
KEY_READ,
|
||||
&mut hkey,
|
||||
);
|
||||
|
||||
if result != 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Query the Install_Dir64 value
|
||||
let value_name: Vec<u16> = OsStr::new("Install_Dir64")
|
||||
.encode_wide()
|
||||
.chain(std::iter::once(0))
|
||||
.collect();
|
||||
|
||||
let mut value_type = 0u32;
|
||||
let mut data_size = 0u32;
|
||||
|
||||
// First call to get the size
|
||||
let result = winreg::RegQueryValueExW(
|
||||
hkey,
|
||||
value_name.as_ptr(),
|
||||
std::ptr::null_mut(),
|
||||
&mut value_type,
|
||||
std::ptr::null_mut(),
|
||||
&mut data_size,
|
||||
);
|
||||
|
||||
if result != 0 || data_size == 0 {
|
||||
winreg::RegCloseKey(hkey);
|
||||
return None;
|
||||
}
|
||||
|
||||
// Allocate buffer and read the value
|
||||
let mut buffer: Vec<u16> = vec![0; (data_size / 2) as usize];
|
||||
let result = winreg::RegQueryValueExW(
|
||||
hkey,
|
||||
value_name.as_ptr(),
|
||||
std::ptr::null_mut(),
|
||||
&mut value_type,
|
||||
buffer.as_mut_ptr() as *mut u8,
|
||||
&mut data_size,
|
||||
);
|
||||
|
||||
winreg::RegCloseKey(hkey);
|
||||
|
||||
if result == 0 {
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -250,8 +250,18 @@ FunctionEnd
|
|||
; Uninstaller
|
||||
|
||||
function un.onInit
|
||||
MessageBox MB_OKCANCEL "This will remove Anki's program files, but will not delete your card data. If you wish to delete your card data as well, you can do so via File>Switch Profile inside Anki first. Are you sure you wish to uninstall Anki?" /SD IDOK IDOK next
|
||||
Quit
|
||||
; Check for ANKI_LAUNCHER environment variable
|
||||
ReadEnvStr $R0 "ANKI_LAUNCHER"
|
||||
${If} $R0 != ""
|
||||
; Wait for launcher to exit
|
||||
Sleep 2000
|
||||
Goto next
|
||||
${Else}
|
||||
; Try to launch anki.exe with ANKI_LAUNCHER_UNINSTALL=1
|
||||
IfFileExists "$INSTDIR\anki.exe" 0 next
|
||||
nsExec::Exec 'cmd /c "set ANKI_LAUNCHER_UNINSTALL=1 && start /b "" "$INSTDIR\anki.exe""'
|
||||
Quit
|
||||
${EndIf}
|
||||
next:
|
||||
functionEnd
|
||||
|
||||
|
|
Loading…
Reference in a new issue