Merge branch 'main' into fix-tag-editor-windows-qt68

This commit is contained in:
Abdo 2025-06-25 14:04:28 +03:00 committed by GitHub
commit 5fcd8398db
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 540 additions and 114 deletions

View file

@ -138,7 +138,7 @@ unic-ucd-category = "0.9.0"
unicode-normalization = "0.1.24" unicode-normalization = "0.1.24"
walkdir = "2.5.0" walkdir = "2.5.0"
which = "8.0.0" which = "8.0.0"
winapi = { version = "0.3", features = ["wincon"] } winapi = { version = "0.3", features = ["wincon", "errhandlingapi", "consoleapi"] }
windows = { version = "0.61.3", features = ["Media_SpeechSynthesis", "Media_Core", "Foundation_Collections", "Storage_Streams"] } windows = { version = "0.61.3", features = ["Media_SpeechSynthesis", "Media_Core", "Foundation_Collections", "Storage_Streams"] }
wiremock = "0.6.3" wiremock = "0.6.3"
xz2 = "0.1.7" xz2 = "0.1.7"

View file

@ -23,47 +23,45 @@ def first_run_setup() -> None:
if not is_mac: if not is_mac:
return return
def _dot(): # Import anki_audio first and spawn commands
print(".", flush=True, end="")
_dot()
import anki.collection
_dot()
import PyQt6.sip
_dot()
import PyQt6.QtCore
_dot()
import PyQt6.QtGui
_dot()
import PyQt6.QtNetwork
_dot()
import PyQt6.QtQuick
_dot()
import PyQt6.QtWebChannel
_dot()
import PyQt6.QtWebEngineCore
_dot()
import PyQt6.QtWebEngineWidgets
_dot()
import anki_audio import anki_audio
import PyQt6.QtWidgets
audio_pkg_path = Path(anki_audio.__file__).parent audio_pkg_path = Path(anki_audio.__file__).parent
# Invoke mpv and lame # Start mpv and lame commands concurrently
cmd = [Path(""), "--version"] processes = []
for cmd_name in ["mpv", "lame"]: for cmd_name in ["mpv", "lame"]:
_dot() cmd_path = audio_pkg_path / cmd_name
cmd[0] = audio_pkg_path / cmd_name proc = subprocess.Popen(
subprocess.run([str(cmd[0]), str(cmd[1])], check=True, capture_output=True) [str(cmd_path), "--version"],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
processes.append(proc)
print() # Continue with other imports while commands run
import concurrent.futures
import bs4
import flask
import flask_cors
import markdown
import PyQt6.QtCore
import PyQt6.QtGui
import PyQt6.QtNetwork
import PyQt6.QtQuick
import PyQt6.QtWebChannel
import PyQt6.QtWebEngineCore
import PyQt6.QtWebEngineWidgets
import PyQt6.QtWidgets
import PyQt6.sip
import requests
import waitress
import anki.collection
from . import _macos_helper
# Wait for both commands to complete
for proc in processes:
proc.wait()

View file

@ -1,9 +1,15 @@
#!/bin/bash #!/bin/bash
#
# This script currently only supports universal builds on x86_64.
#
set -e set -e
# Add Linux cross-compilation target # Add Linux cross-compilation target
rustup target add aarch64-unknown-linux-gnu rustup target add aarch64-unknown-linux-gnu
# Detect host architecture
HOST_ARCH=$(uname -m)
# Define output paths # Define output paths
OUTPUT_DIR="../../../out/launcher" OUTPUT_DIR="../../../out/launcher"
@ -12,11 +18,18 @@ LAUNCHER_DIR="$OUTPUT_DIR/anki-launcher"
# Clean existing output directory # Clean existing output directory
rm -rf "$LAUNCHER_DIR" rm -rf "$LAUNCHER_DIR"
# Build binaries for both Linux architectures # Build binaries based on host architecture
cargo build -p launcher --release --target x86_64-unknown-linux-gnu if [ "$HOST_ARCH" = "aarch64" ]; then
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc \ # On aarch64 host, only build for aarch64
cargo build -p launcher --release --target aarch64-unknown-linux-gnu cargo build -p launcher --release --target aarch64-unknown-linux-gnu
(cd ../../.. && ./ninja extract:uv_lin_arm) else
# On other hosts, build for both architectures
cargo build -p launcher --release --target x86_64-unknown-linux-gnu
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc \
cargo build -p launcher --release --target aarch64-unknown-linux-gnu
# Extract uv_lin_arm for cross-compilation
(cd ../../.. && ./ninja extract:uv_lin_arm)
fi
# Create output directory # Create output directory
mkdir -p "$LAUNCHER_DIR" mkdir -p "$LAUNCHER_DIR"
@ -24,13 +37,21 @@ mkdir -p "$LAUNCHER_DIR"
# Copy binaries and support files # Copy binaries and support files
TARGET_DIR=${CARGO_TARGET_DIR:-../../../target} TARGET_DIR=${CARGO_TARGET_DIR:-../../../target}
# Copy launcher binaries with architecture suffixes # Copy binaries with architecture suffixes
cp "$TARGET_DIR/x86_64-unknown-linux-gnu/release/launcher" "$LAUNCHER_DIR/launcher.amd64" if [ "$HOST_ARCH" = "aarch64" ]; then
cp "$TARGET_DIR/aarch64-unknown-linux-gnu/release/launcher" "$LAUNCHER_DIR/launcher.arm64" # On aarch64 host, copy arm64 binary to both locations
cp "$TARGET_DIR/aarch64-unknown-linux-gnu/release/launcher" "$LAUNCHER_DIR/launcher.amd64"
# Copy uv binaries with architecture suffixes cp "$TARGET_DIR/aarch64-unknown-linux-gnu/release/launcher" "$LAUNCHER_DIR/launcher.arm64"
cp "../../../out/extracted/uv/uv" "$LAUNCHER_DIR/uv.amd64" # Copy uv binary to both locations
cp "../../../out/extracted/uv_lin_arm/uv" "$LAUNCHER_DIR/uv.arm64" cp "../../../out/extracted/uv/uv" "$LAUNCHER_DIR/uv.amd64"
cp "../../../out/extracted/uv/uv" "$LAUNCHER_DIR/uv.arm64"
else
# On other hosts, copy architecture-specific binaries
cp "$TARGET_DIR/x86_64-unknown-linux-gnu/release/launcher" "$LAUNCHER_DIR/launcher.amd64"
cp "$TARGET_DIR/aarch64-unknown-linux-gnu/release/launcher" "$LAUNCHER_DIR/launcher.arm64"
cp "../../../out/extracted/uv/uv" "$LAUNCHER_DIR/uv.amd64"
cp "../../../out/extracted/uv_lin_arm/uv" "$LAUNCHER_DIR/uv.arm64"
fi
# Copy support files from lin directory # Copy support files from lin directory
for file in README.md anki.1 anki.desktop anki.png anki.xml anki.xpm install.sh uninstall.sh anki; do for file in README.md anki.1 anki.desktop anki.png anki.xml anki.xpm install.sh uninstall.sh anki; do

View file

@ -47,8 +47,8 @@ done
codesign -vvv "$APP_LAUNCHER" codesign -vvv "$APP_LAUNCHER"
spctl -a "$APP_LAUNCHER" spctl -a "$APP_LAUNCHER"
# Notarize # Notarize and bundle (skip if NODMG is set)
./notarize.sh "$OUTPUT_DIR" if [ -z "$NODMG" ]; then
./notarize.sh "$OUTPUT_DIR"
# Bundle ./dmg/build.sh "$OUTPUT_DIR"
./dmg/build.sh "$OUTPUT_DIR" fi

View file

@ -114,10 +114,13 @@ fn copy_files(output_dir: &Path) -> Result<()> {
let launcher_dst = output_dir.join("anki.exe"); let launcher_dst = output_dir.join("anki.exe");
copy_file(&launcher_src, &launcher_dst)?; copy_file(&launcher_src, &launcher_dst)?;
// Copy uv.exe // Copy uv.exe and uvw.exe
let uv_src = PathBuf::from("../../../out/extracted/uv/uv.exe"); let uv_src = PathBuf::from("../../../out/extracted/uv/uv.exe");
let uv_dst = output_dir.join("uv.exe"); let uv_dst = output_dir.join("uv.exe");
copy_file(&uv_src, &uv_dst)?; copy_file(&uv_src, &uv_dst)?;
let uv_src = PathBuf::from("../../../out/extracted/uv/uvw.exe");
let uv_dst = output_dir.join("uvw.exe");
copy_file(&uv_src, &uv_dst)?;
println!("Copying support files..."); println!("Copying support files...");

View file

@ -4,28 +4,48 @@
#![windows_subsystem = "windows"] #![windows_subsystem = "windows"]
use std::io::stdin; use std::io::stdin;
use std::io::stdout;
use std::io::Write;
use std::process::Command; use std::process::Command;
use std::time::SystemTime;
use std::time::UNIX_EPOCH;
use anki_io::copy_if_newer; use anki_io::copy_if_newer;
use anki_io::create_dir_all; use anki_io::create_dir_all;
use anki_io::modified_time; use anki_io::modified_time;
use anki_io::read_file;
use anki_io::remove_file; use anki_io::remove_file;
use anki_io::write_file; use anki_io::write_file;
use anki_process::CommandExt; use anki_process::CommandExt;
use anyhow::Context; use anyhow::Context;
use anyhow::Result; use anyhow::Result;
use crate::platform::ensure_terminal_shown;
use crate::platform::exec_anki; use crate::platform::exec_anki;
use crate::platform::get_anki_binary_path; use crate::platform::get_anki_binary_path;
use crate::platform::get_exe_and_resources_dirs; use crate::platform::get_exe_and_resources_dirs;
use crate::platform::get_uv_binary_name; use crate::platform::get_uv_binary_name;
use crate::platform::handle_first_launch; use crate::platform::handle_first_launch;
use crate::platform::handle_terminal_launch;
use crate::platform::initial_terminal_setup; use crate::platform::initial_terminal_setup;
use crate::platform::launch_anki_detached; use crate::platform::launch_anki_detached;
mod platform; mod platform;
#[derive(Debug, Clone)]
pub enum VersionKind {
PyOxidizer(String),
Uv(String),
}
#[derive(Debug, Clone)]
pub enum MainMenuChoice {
Latest,
KeepExisting,
Version(VersionKind),
ToggleBetas,
Quit,
}
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct Config { pub struct Config {
pub show_console: bool, pub show_console: bool,
@ -33,6 +53,9 @@ pub struct Config {
fn main() { fn main() {
if let Err(e) = run() { if let Err(e) = run() {
let mut config: Config = Config::default();
initial_terminal_setup(&mut config);
eprintln!("Error: {:#}", e); eprintln!("Error: {:#}", e);
eprintln!("Press enter to close..."); eprintln!("Press enter to close...");
let mut input = String::new(); let mut input = String::new();
@ -43,8 +66,7 @@ fn main() {
} }
fn run() -> Result<()> { fn run() -> Result<()> {
let mut config = Config::default(); let mut config: Config = Config::default();
initial_terminal_setup(&mut config);
let uv_install_root = dirs::data_local_dir() let uv_install_root = dirs::data_local_dir()
.context("Unable to determine data_dir")? .context("Unable to determine data_dir")?
@ -62,60 +84,322 @@ fn run() -> Result<()> {
// Create install directory and copy project files in // Create install directory and copy project files in
create_dir_all(&uv_install_root)?; create_dir_all(&uv_install_root)?;
let had_user_pyproj = user_pyproject_path.exists();
if !had_user_pyproj {
// during initial launcher testing, enable betas by default
write_file(&prerelease_marker, "")?;
}
copy_if_newer(&dist_pyproject_path, &user_pyproject_path)?; copy_if_newer(&dist_pyproject_path, &user_pyproject_path)?;
copy_if_newer(&dist_python_version_path, &user_python_version_path)?; copy_if_newer(&dist_python_version_path, &user_python_version_path)?;
let pyproject_has_changed = let pyproject_has_changed = !sync_complete_marker.exists() || {
!user_pyproject_path.exists() || !sync_complete_marker.exists() || { let pyproject_toml_time = modified_time(&user_pyproject_path)?;
let pyproject_toml_time = modified_time(&user_pyproject_path)?; let sync_complete_time = modified_time(&sync_complete_marker)?;
let sync_complete_time = modified_time(&sync_complete_marker)?; Ok::<bool, anyhow::Error>(pyproject_toml_time > sync_complete_time)
Ok::<bool, anyhow::Error>(pyproject_toml_time > sync_complete_time) }
} .unwrap_or(true);
.unwrap_or(true);
if !pyproject_has_changed { if !pyproject_has_changed {
// If venv is already up to date, exec as normal // If venv is already up to date, exec as normal
initial_terminal_setup(&mut config);
let anki_bin = get_anki_binary_path(&uv_install_root); let anki_bin = get_anki_binary_path(&uv_install_root);
exec_anki(&anki_bin, &config)?; exec_anki(&anki_bin, &config)?;
return Ok(()); return Ok(());
} }
// we'll need to launch uv; reinvoke ourselves in a terminal so the user can see // we'll need to launch uv; reinvoke ourselves in a terminal so the user can see
handle_terminal_launch()?; ensure_terminal_shown()?;
print!("\x1B[2J\x1B[H"); // Clear screen and move cursor to top
println!("\x1B[1mAnki Launcher\x1B[0m\n");
// Remove sync marker before attempting sync // Check if there's an existing installation before removing marker
let _ = remove_file(&sync_complete_marker); let has_existing_install = sync_complete_marker.exists();
// Sync the venv loop {
let mut command = Command::new(&uv_path); let menu_choice = get_main_menu_choice(has_existing_install, &prerelease_marker);
command
.current_dir(&uv_install_root)
.args(["sync", "--upgrade", "--managed-python"]);
// Set UV_PRERELEASE=allow if prerelease file exists match menu_choice {
if prerelease_marker.exists() { MainMenuChoice::Quit => std::process::exit(0),
command.env("UV_PRERELEASE", "allow"); MainMenuChoice::KeepExisting => {
// Skip sync, just launch existing installation
break;
}
MainMenuChoice::ToggleBetas => {
// Toggle beta prerelease file
if prerelease_marker.exists() {
let _ = remove_file(&prerelease_marker);
println!("Beta releases disabled.");
} else {
write_file(&prerelease_marker, "")?;
println!("Beta releases enabled.");
}
println!();
continue;
}
_ => {
// For other choices, update project files and sync
update_pyproject_for_version(
menu_choice.clone(),
dist_pyproject_path.clone(),
user_pyproject_path.clone(),
dist_python_version_path.clone(),
user_python_version_path.clone(),
)?;
// Remove sync marker before attempting sync
let _ = remove_file(&sync_complete_marker);
// Sync the venv
let mut command = Command::new(&uv_path);
command.current_dir(&uv_install_root).args([
"sync",
"--upgrade",
"--managed-python",
]);
// Add python version if .python-version file exists
if user_python_version_path.exists() {
let python_version = read_file(&user_python_version_path)?;
let python_version_str = String::from_utf8(python_version)
.context("Invalid UTF-8 in .python-version")?;
let python_version_trimmed = python_version_str.trim();
command.args(["--python", python_version_trimmed]);
}
// Set UV_PRERELEASE=allow if beta mode is enabled
if prerelease_marker.exists() {
command.env("UV_PRERELEASE", "allow");
}
println!("\x1B[1mUpdating Anki...\x1B[0m\n");
match command.ensure_success() {
Ok(_) => {
// Sync succeeded, break out of loop
break;
}
Err(e) => {
// If sync fails due to things like a missing wheel on pypi,
// we need to remove the lockfile or uv will cache the bad result.
let _ = remove_file(&uv_lock_path);
println!("Install failed: {:#}", e);
println!();
continue;
}
}
}
}
} }
// temporarily force it on during initial beta testing // Write marker file to indicate we've completed the sync process
command.env("UV_PRERELEASE", "allow"); write_sync_marker(&sync_complete_marker)?;
if let Err(e) = command.ensure_success() {
// If sync fails due to things like a missing wheel on pypi,
// we need to remove the lockfile or uv will cache the bad result.
let _ = remove_file(&uv_lock_path);
return Err(e.into());
}
// Write marker file to indicate successful sync
write_file(&sync_complete_marker, "")?;
// First launch // First launch
let anki_bin = get_anki_binary_path(&uv_install_root); let anki_bin = get_anki_binary_path(&uv_install_root);
handle_first_launch(&anki_bin)?; handle_first_launch(&anki_bin)?;
println!("\nPress enter to start Anki.");
let mut input = String::new();
let _ = stdin().read_line(&mut input);
// Then launch the binary as detached subprocess so the terminal can close // Then launch the binary as detached subprocess so the terminal can close
launch_anki_detached(&anki_bin, &config)?; launch_anki_detached(&anki_bin, &config)?;
Ok(()) Ok(())
} }
fn write_sync_marker(sync_complete_marker: &std::path::Path) -> Result<()> {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.context("Failed to get system time")?
.as_secs();
write_file(sync_complete_marker, timestamp.to_string())?;
Ok(())
}
fn get_main_menu_choice(
has_existing_install: bool,
prerelease_marker: &std::path::Path,
) -> MainMenuChoice {
loop {
println!("1) Latest Anki (just press enter)");
println!("2) Choose a version");
if has_existing_install {
println!("3) Keep existing version");
}
println!();
let betas_enabled = prerelease_marker.exists();
println!(
"4) Allow betas: {}",
if betas_enabled { "on" } else { "off" }
);
println!("5) Quit");
print!("> ");
let _ = stdout().flush();
let mut input = String::new();
let _ = stdin().read_line(&mut input);
let input = input.trim();
println!();
return match input {
"" | "1" => MainMenuChoice::Latest,
"2" => MainMenuChoice::Version(get_version_kind()),
"3" => {
if has_existing_install {
MainMenuChoice::KeepExisting
} else {
println!("Invalid input. Please try again.\n");
continue;
}
}
"4" => MainMenuChoice::ToggleBetas,
"5" => MainMenuChoice::Quit,
_ => {
println!("Invalid input. Please try again.");
continue;
}
};
}
}
fn get_version_kind() -> VersionKind {
loop {
println!("Enter the version you want to install:");
print!("> ");
let _ = stdout().flush();
let mut input = String::new();
let _ = stdin().read_line(&mut input);
let input = input.trim();
if input.is_empty() {
println!("Please enter a version.");
continue;
}
match parse_version_kind(input) {
Some(version_kind) => {
println!();
return version_kind;
}
None => {
println!("Invalid version format. Please enter a version like 24.10 or 25.06.1 (minimum 2.1.50)");
continue;
}
}
}
}
fn update_pyproject_for_version(
menu_choice: MainMenuChoice,
dist_pyproject_path: std::path::PathBuf,
user_pyproject_path: std::path::PathBuf,
dist_python_version_path: std::path::PathBuf,
user_python_version_path: std::path::PathBuf,
) -> Result<()> {
match menu_choice {
MainMenuChoice::Latest => {
let content = read_file(&dist_pyproject_path)?;
write_file(&user_pyproject_path, &content)?;
let python_version_content = read_file(&dist_python_version_path)?;
write_file(&user_python_version_path, &python_version_content)?;
}
MainMenuChoice::KeepExisting => {
// Do nothing - keep existing pyproject.toml and .python-version
}
MainMenuChoice::ToggleBetas => {
// This should not be reached as ToggleBetas is handled in the loop
unreachable!("ToggleBetas should be handled in the main loop");
}
MainMenuChoice::Version(version_kind) => {
let content = read_file(&dist_pyproject_path)?;
let content_str =
String::from_utf8(content).context("Invalid UTF-8 in pyproject.toml")?;
let updated_content = match &version_kind {
VersionKind::PyOxidizer(version) => {
// Replace package name and add PyQt6 dependencies
content_str.replace(
"anki-release",
&format!(
concat!(
"aqt[qt6]=={}\",\n",
" \"pyqt6==6.6.1\",\n",
" \"pyqt6-qt6==6.6.2\",\n",
" \"pyqt6-webengine==6.6.0\",\n",
" \"pyqt6-webengine-qt6==6.6.2\",\n",
" \"pyqt6_sip==13.6.0"
),
version
),
)
}
VersionKind::Uv(version) => {
content_str.replace("anki-release", &format!("anki-release=={}", version))
}
};
write_file(&user_pyproject_path, &updated_content)?;
// Update .python-version based on version kind
match &version_kind {
VersionKind::PyOxidizer(_) => {
write_file(&user_python_version_path, "3.9")?;
}
VersionKind::Uv(_) => {
copy_if_newer(&dist_python_version_path, &user_python_version_path)?;
}
}
}
MainMenuChoice::Quit => {
std::process::exit(0);
}
}
Ok(())
}
fn parse_version_kind(version: &str) -> Option<VersionKind> {
let numeric_chars: String = version
.chars()
.filter(|c| c.is_ascii_digit() || *c == '.')
.collect();
let parts: Vec<&str> = numeric_chars.split('.').collect();
if parts.len() < 2 {
return None;
}
let major: u32 = match parts[0].parse() {
Ok(val) => val,
Err(_) => return None,
};
let minor: u32 = match parts[1].parse() {
Ok(val) => val,
Err(_) => return None,
};
let patch: u32 = if parts.len() >= 3 {
match parts[2].parse() {
Ok(val) => val,
Err(_) => return None,
}
} else {
0 // Default patch to 0 if not provided
};
// Reject versions < 2.1.50
if major == 2 && (minor != 1 || patch < 50) {
return None;
}
if major < 25 || (major == 25 && minor < 6) {
Some(VersionKind::PyOxidizer(version.to_string()))
} else {
Some(VersionKind::Uv(version.to_string()))
}
}

View file

@ -3,6 +3,11 @@
use std::os::unix::process::CommandExt; use std::os::unix::process::CommandExt;
use std::process::Command; use std::process::Command;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::thread;
use std::time::Duration;
use anki_process::CommandExt as AnkiCommandExt; use anki_process::CommandExt as AnkiCommandExt;
use anyhow::Context; use anyhow::Context;
@ -10,6 +15,7 @@ use anyhow::Result;
// Re-export Unix functions that macOS uses // Re-export Unix functions that macOS uses
pub use super::unix::{ pub use super::unix::{
ensure_terminal_shown,
exec_anki, exec_anki,
get_anki_binary_path, get_anki_binary_path,
initial_terminal_setup, initial_terminal_setup,
@ -25,22 +31,13 @@ pub fn launch_anki_detached(anki_bin: &std::path::Path, _config: &crate::Config)
.process_group(0) .process_group(0)
.ensure_spawn()?; .ensure_spawn()?;
std::mem::forget(child); std::mem::forget(child);
println!("Anki will start shortly.");
println!("\x1B[1mYou can close this window.\x1B[0m\n");
Ok(()) Ok(())
} }
pub fn handle_terminal_launch() -> Result<()> { pub fn relaunch_in_terminal() -> Result<()> {
let stdout_is_terminal = std::io::IsTerminal::is_terminal(&std::io::stdout());
if stdout_is_terminal {
print!("\x1B[2J\x1B[H"); // Clear screen and move cursor to top
println!("\x1B[1mPreparing to start Anki...\x1B[0m\n");
} else {
// If launched from GUI, relaunch in Terminal.app
relaunch_in_terminal()?;
}
Ok(())
}
fn relaunch_in_terminal() -> Result<()> {
let current_exe = std::env::current_exe().context("Failed to get current executable path")?; let current_exe = std::env::current_exe().context("Failed to get current executable path")?;
Command::new("open") Command::new("open")
.args(["-a", "Terminal"]) .args(["-a", "Terminal"])
@ -50,12 +47,37 @@ fn relaunch_in_terminal() -> Result<()> {
} }
pub fn handle_first_launch(anki_bin: &std::path::Path) -> Result<()> { pub fn handle_first_launch(anki_bin: &std::path::Path) -> Result<()> {
use std::io::Write;
use std::io::{
self,
};
// Pre-validate by running --version to trigger any Gatekeeper checks // Pre-validate by running --version to trigger any Gatekeeper checks
println!("\n\x1B[1mThis may take a few minutes. Please wait...\x1B[0m"); print!("\n\x1B[1mThis may take a few minutes. Please wait\x1B[0m");
io::stdout().flush().unwrap();
// Start progress indicator
let running = Arc::new(AtomicBool::new(true));
let running_clone = running.clone();
let progress_thread = thread::spawn(move || {
while running_clone.load(Ordering::Relaxed) {
print!(".");
io::stdout().flush().unwrap();
thread::sleep(Duration::from_secs(1));
}
});
let _ = Command::new(anki_bin) let _ = Command::new(anki_bin)
.env("ANKI_FIRST_RUN", "1") .env("ANKI_FIRST_RUN", "1")
.arg("--version") .arg("--version")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.ensure_success(); .ensure_success();
// Stop progress indicator
running.store(false, Ordering::Relaxed);
progress_thread.join().unwrap();
println!(); // New line after dots
Ok(()) Ok(())
} }

View file

@ -3,6 +3,7 @@
#![allow(dead_code)] #![allow(dead_code)]
use std::io::IsTerminal;
use std::path::PathBuf; use std::path::PathBuf;
use std::process::Command; use std::process::Command;
@ -16,11 +17,59 @@ pub fn initial_terminal_setup(_config: &mut Config) {
// No special terminal setup needed on Unix // No special terminal setup needed on Unix
} }
pub fn handle_terminal_launch() -> Result<()> { pub fn ensure_terminal_shown() -> Result<()> {
print!("\x1B[2J\x1B[H"); // Clear screen and move cursor to top let stdout_is_terminal = IsTerminal::is_terminal(&std::io::stdout());
println!("\x1B[1mPreparing to start Anki...\x1B[0m\n"); if !stdout_is_terminal {
// Skip terminal relaunch on non-macOS Unix systems as we don't know which // If launched from GUI, try to relaunch in a terminal
// terminal is installed crate::platform::relaunch_in_terminal()?;
}
// Set terminal title to "Anki Launcher"
print!("\x1b]2;Anki Launcher\x07");
Ok(())
}
#[cfg(not(target_os = "macos"))]
pub fn relaunch_in_terminal() -> Result<()> {
let current_exe = std::env::current_exe().context("Failed to get current executable path")?;
// Try terminals in order of preference
let terminals = [
("x-terminal-emulator", vec!["-e"]),
("gnome-terminal", vec!["--"]),
("konsole", vec!["-e"]),
("xfce4-terminal", vec!["-e"]),
("alacritty", vec!["-e"]),
("kitty", vec![]),
("foot", vec![]),
("urxvt", vec!["-e"]),
("xterm", vec!["-e"]),
];
for (terminal_cmd, args) in &terminals {
// Check if terminal exists
if Command::new("which")
.arg(terminal_cmd)
.output()
.map(|o| o.status.success())
.unwrap_or(false)
{
// Try to launch the terminal
let mut cmd = Command::new(terminal_cmd);
if args.is_empty() {
cmd.arg(&current_exe);
} else {
cmd.args(args).arg(&current_exe);
}
if cmd.spawn().is_ok() {
std::process::exit(0);
}
}
}
// If no terminal worked, continue without relaunching
Ok(()) Ok(())
} }

View file

@ -7,27 +7,77 @@ use std::process::Command;
use anki_process::CommandExt; use anki_process::CommandExt;
use anyhow::Context; use anyhow::Context;
use anyhow::Result; use anyhow::Result;
use winapi::um::consoleapi;
use winapi::um::errhandlingapi;
use winapi::um::wincon;
use crate::Config; use crate::Config;
pub fn handle_terminal_launch() -> Result<()> { pub fn ensure_terminal_shown() -> Result<()> {
// uv will do this itself ensure_console();
// // Check if we're already relaunched to prevent infinite recursion
// if std::env::var("ANKI_LAUNCHER_IN_TERMINAL").is_ok() {
// println!("Recurse: Preparing to start Anki...\n");
// return Ok(());
// }
// if have_console {
// } else {
// relaunch_in_cmd()?;
// }
Ok(()) Ok(())
} }
fn ensure_console() {
unsafe {
if !wincon::GetConsoleWindow().is_null() {
return;
}
if consoleapi::AllocConsole() == 0 {
let error_code = errhandlingapi::GetLastError();
eprintln!("unexpected AllocConsole error: {}", error_code);
return;
}
// 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.
let _ = Command::new("cmd").args(&["/C", ""]).status();
}
}
fn attach_to_parent_console() -> bool {
unsafe {
if !wincon::GetConsoleWindow().is_null() {
// we have a console already
println!("attach: already had console, false");
return false;
}
if wincon::AttachConsole(wincon::ATTACH_PARENT_PROCESS) != 0 {
// successfully attached to parent
println!("attach: true");
true
} else {
println!("attach: false");
false
}
}
}
/// If parent process has a console (eg cmd.exe), redirect our output there. /// If parent process has a console (eg cmd.exe), redirect our output there.
/// Sets config.show_console to true if successfully attached to console. /// Sets config.show_console to true if successfully attached to console.
pub fn initial_terminal_setup(config: &mut Config) { pub fn initial_terminal_setup(config: &mut Config) {
use std::ffi::CString; use std::ffi::CString;
use libc_stdhandle::*; use libc_stdhandle::*;
use winapi::um::wincon;
let console_attached = unsafe { wincon::AttachConsole(wincon::ATTACH_PARENT_PROCESS) }; if !attach_to_parent_console() {
if console_attached == 0 {
return; return;
} }
// we launched without a console, so we'll need to open stdin/out/err
let conin = CString::new("CONIN$").unwrap(); let conin = CString::new("CONIN$").unwrap();
let conout = CString::new("CONOUT$").unwrap(); let conout = CString::new("CONOUT$").unwrap();
let r = CString::new("r").unwrap(); let r = CString::new("r").unwrap();
@ -113,6 +163,5 @@ pub fn get_exe_and_resources_dirs() -> Result<(PathBuf, PathBuf)> {
} }
pub fn get_uv_binary_name() -> &'static str { pub fn get_uv_binary_name() -> &'static str {
// Windows uses standard uv binary name
"uv.exe" "uv.exe"
} }