mirror of
https://github.com/ankitects/anki.git
synced 2026-01-13 14:03:55 -05:00
Merge branch 'main' into fix-tag-editor-windows-qt68
This commit is contained in:
commit
5fcd8398db
9 changed files with 540 additions and 114 deletions
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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...");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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(¤t_exe);
|
||||||
|
} else {
|
||||||
|
cmd.args(args).arg(¤t_exe);
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.spawn().is_ok() {
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no terminal worked, continue without relaunching
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue