This commit is contained in:
llama 2025-09-24 15:36:02 +00:00 committed by GitHub
commit 9e813574f4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 161 additions and 44 deletions

35
Cargo.lock generated
View file

@ -3555,6 +3555,7 @@ dependencies = [
name = "launcher"
version = "1.0.0"
dependencies = [
"anki_i18n",
"anki_io",
"anki_process",
"anyhow",
@ -3563,6 +3564,7 @@ dependencies = [
"embed-resource",
"libc",
"libc-stdhandle",
"locale_config",
"serde_json",
"widestring",
"windows 0.61.3",
@ -3702,6 +3704,19 @@ version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5e54036fe321fd421e10d732f155734c4e4afd610dd556d9a82833ab3ee0bed"
[[package]]
name = "locale_config"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d2c35b16f4483f6c26f0e4e9550717a2f6575bcd6f12a53ff0c490a94a6934"
dependencies = [
"lazy_static",
"objc",
"objc-foundation",
"regex",
"winapi",
]
[[package]]
name = "lock_api"
version = "0.4.13"
@ -4380,6 +4395,26 @@ dependencies = [
"malloc_buf",
]
[[package]]
name = "objc-foundation"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9"
dependencies = [
"block",
"objc",
"objc_id",
]
[[package]]
name = "objc_id"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b"
dependencies = [
"objc",
]
[[package]]
name = "object"
version = "0.36.7"

View file

@ -92,6 +92,7 @@ itertools = "0.14.0"
junction = "1.2.0"
libc = "0.2"
libc-stdhandle = "0.1"
locale_config = "0.3.0"
maplit = "1.0.2"
nom = "8.0.0"
num-format = "0.4.4"

View file

@ -2226,7 +2226,7 @@
{
"authors": "Ibraheem Ahmed <ibraheem@ibraheem.ca>",
"description": "A high performance, zero-copy URL router.",
"license": "MIT AND BSD-3-Clause",
"license": "BSD-3-Clause AND MIT",
"license_file": null,
"name": "matchit",
"repository": "https://github.com/ibraheemdev/matchit"
@ -4154,7 +4154,7 @@
{
"authors": "David Tolnay <dtolnay@gmail.com>",
"description": "Determine whether characters have the XID_Start or XID_Continue properties according to Unicode Standard Annex #31",
"license": "(MIT OR Apache-2.0) AND Unicode-3.0",
"license": "(Apache-2.0 OR MIT) AND Unicode-3.0",
"license_file": null,
"name": "unicode-ident",
"repository": "https://github.com/dtolnay/unicode-ident"

33
ftl/core/launcher.ftl Normal file
View file

@ -0,0 +1,33 @@
launcher-title = Anki Launcher
launcher-press-enter-to-start = Press enter to start Anki.
launcher-anki-will-start-shortly = Anki will start shortly.
launcher-you-can-close-this-window = You can close this window.
launcher-updating-anki = Updating Anki...
launcher-latest-anki = Latest Anki (just press Enter)
launcher-choose-a-version = Choose a version
launcher-sync-project-changes = Sync project changes
launcher-keep-existing-version = Keep existing version ({ $current })
launcher-revert-to-previous = Revert to previous version ({ $prev })
launcher-allow-betas = Allow betas: { $state }
launcher-on = on
launcher-off = off
launcher-cache-downloads = Cache downloads: { $state }
launcher-download-mirror = Download mirror: { $state }
launcher-uninstall = Uninstall
launcher-invalid-input = Invalid input. Please try again.
launcher-latest-releases = Latest releases: { $releases }
launcher-enter-the-version-you-want = Enter the version you want to install:
launcher-versions-before-cant-be-installed = Versions before 2.1.50 can't be installed.
launcher-invalid-version = Invalid version.
launcher-unable-to-check-for-versions = Unable to check for Anki versions. Please check your internet connection.
launcher-checking-for-updates = Checking for updates...
launcher-uninstall-confirm = Uninstall Anki's program files? (y/n)
launcher-uninstall-cancelled = Uninstall cancelled.
launcher-program-files-removed = Program files removed.
launcher-remove-all-profiles-confirm = Remove all profiles/cards? (y/n)
launcher-user-data-removed = User data removed.
launcher-download-mirror-options = Download mirror options:
launcher-mirror-no-mirror = No mirror
launcher-mirror-china = China
launcher-mirror-disabled = Mirror disabled.
launcher-mirror-china-enabled = China mirror enabled.

View file

@ -8,11 +8,13 @@ publish = false
rust-version.workspace = true
[dependencies]
anki_i18n.workspace = true
anki_io.workspace = true
anki_process.workspace = true
anyhow.workspace = true
camino.workspace = true
dirs.workspace = true
locale_config.workspace = true
serde_json.workspace = true
[target.'cfg(all(unix, not(target_os = "macos")))'.dependencies]

View file

@ -10,6 +10,7 @@ use std::process::Command;
use std::time::SystemTime;
use std::time::UNIX_EPOCH;
use anki_i18n::I18n;
use anki_io::copy_file;
use anki_io::create_dir_all;
use anki_io::modified_time;
@ -31,6 +32,7 @@ use crate::platform::respawn_launcher;
mod platform;
struct State {
tr: I18n,
current_version: Option<String>,
prerelease_marker: std::path::PathBuf,
uv_install_root: std::path::PathBuf,
@ -100,7 +102,14 @@ fn run() -> Result<()> {
let (exe_dir, resources_dir) = get_exe_and_resources_dirs()?;
let locale = locale_config::Locale::user_default().to_string();
let mut state = State {
tr: I18n::new(&[if !locale.is_empty() {
locale
} else {
"en".to_owned()
}]),
current_version: None,
prerelease_marker: uv_install_root.join("prerelease"),
uv_install_root: uv_install_root.clone(),
@ -160,7 +169,7 @@ fn run() -> Result<()> {
}
print!("\x1B[2J\x1B[H"); // Clear screen and move cursor to top
println!("\x1B[1mAnki Launcher\x1B[0m\n");
println!("\x1B[1m{}\x1B[0m\n", state.tr.launcher_title());
ensure_os_supported()?;
@ -178,15 +187,18 @@ fn run() -> Result<()> {
}
if cfg!(unix) && !cfg!(target_os = "macos") {
println!("\nPress enter to start Anki.");
println!("\n{}", state.tr.launcher_press_enter_to_start());
let mut input = String::new();
let _ = stdin().read_line(&mut input);
} else {
// on Windows/macOS, the user needs to close the terminal/console
// currently, but ideas on how we can avoid this would be good!
println!();
println!("Anki will start shortly.");
println!("\x1B[1mYou can close this window.\x1B[0m\n");
println!("{}", state.tr.launcher_anki_will_start_shortly());
println!(
"\x1B[1m{}\x1B[0m\n",
state.tr.launcher_you_can_close_this_window()
);
}
// respawn the launcher as a disconnected subprocess for normal startup
@ -258,7 +270,7 @@ fn handle_version_install_or_update(state: &State, choice: MainMenuChoice) -> Re
// Remove sync marker before attempting sync
let _ = remove_file(&state.sync_complete_marker);
println!("Updating Anki...\n");
println!("{}\n", state.tr.launcher_updating_anki());
let python_version_trimmed = if state.user_python_version_path.exists() {
let python_version = read_file(&state.user_python_version_path)?;
@ -440,44 +452,62 @@ fn file_timestamp_secs(path: &std::path::Path) -> i64 {
fn get_main_menu_choice(state: &State) -> Result<MainMenuChoice> {
loop {
println!("1) Latest Anki (press Enter)");
println!("2) Choose a version");
println!("1) {}", state.tr.launcher_latest_anki());
println!("2) {}", state.tr.launcher_choose_a_version());
if let Some(current_version) = &state.current_version {
let normalized_current = normalize_version(current_version);
if state.pyproject_modified_by_user {
println!("3) Sync project changes");
println!("3) {}", state.tr.launcher_sync_project_changes());
} else {
println!("3) Keep existing version ({normalized_current})");
println!(
"3) {}",
state.tr.launcher_keep_existing_version(normalized_current)
);
}
}
if let Some(prev_version) = &state.previous_version {
if state.current_version.as_ref() != Some(prev_version) {
let normalized_prev = normalize_version(prev_version);
println!("4) Revert to previous version ({normalized_prev})");
println!(
"4) {}",
state.tr.launcher_revert_to_previous(normalized_prev)
);
}
}
println!();
let betas_enabled = state.prerelease_marker.exists();
println!(
"5) Allow betas: {}",
if betas_enabled { "on" } else { "off" }
"5) {}",
state.tr.launcher_allow_betas(if betas_enabled {
state.tr.launcher_on()
} else {
state.tr.launcher_off()
})
);
let cache_enabled = !state.no_cache_marker.exists();
println!(
"6) Cache downloads: {}",
if cache_enabled { "on" } else { "off" }
"6) {}",
state.tr.launcher_cache_downloads(if cache_enabled {
state.tr.launcher_on()
} else {
state.tr.launcher_off()
})
);
let mirror_enabled = is_mirror_enabled(state);
println!(
"7) Download mirror: {}",
if mirror_enabled { "on" } else { "off" }
"7) {}",
state.tr.launcher_download_mirror(if mirror_enabled {
state.tr.launcher_on()
} else {
state.tr.launcher_off()
})
);
println!();
println!("8) Uninstall");
println!("8) {}", state.tr.launcher_uninstall());
print!("> ");
let _ = stdout().flush();
@ -499,7 +529,7 @@ fn get_main_menu_choice(state: &State) -> Result<MainMenuChoice> {
if state.current_version.is_some() {
MainMenuChoice::KeepExisting
} else {
println!("Invalid input. Please try again.\n");
println!("{}\n", state.tr.launcher_invalid_input());
continue;
}
}
@ -511,7 +541,7 @@ fn get_main_menu_choice(state: &State) -> Result<MainMenuChoice> {
}
}
}
println!("Invalid input. Please try again.\n");
println!("{}\n", state.tr.launcher_invalid_input());
continue;
}
"5" => MainMenuChoice::ToggleBetas,
@ -519,7 +549,7 @@ fn get_main_menu_choice(state: &State) -> Result<MainMenuChoice> {
"7" => MainMenuChoice::DownloadMirror,
"8" => MainMenuChoice::Uninstall,
_ => {
println!("Invalid input. Please try again.");
println!("{}\n", state.tr.launcher_invalid_input());
continue;
}
});
@ -534,9 +564,9 @@ fn get_version_kind(state: &State) -> Result<Option<VersionKind>> {
.map(|v| v.as_str())
.collect::<Vec<_>>()
.join(", ");
println!("Latest releases: {releases_str}");
println!("{}", state.tr.launcher_latest_releases(releases_str));
println!("Enter the version you want to install:");
println!("{}", state.tr.launcher_enter_the_version_you_want());
print!("> ");
let _ = stdout().flush();
@ -560,11 +590,11 @@ fn get_version_kind(state: &State) -> Result<Option<VersionKind>> {
Ok(Some(version_kind))
}
(None, true) => {
println!("Versions before 2.1.50 can't be installed.");
println!("{}", state.tr.launcher_versions_before_cant_be_installed());
Ok(None)
}
_ => {
println!("Invalid version.\n");
println!("{}\n", state.tr.launcher_invalid_version());
Ok(None)
}
}
@ -700,7 +730,7 @@ fn fetch_versions(state: &State) -> Result<Vec<String>> {
let output = match cmd.utf8_output() {
Ok(output) => output,
Err(e) => {
print!("Unable to check for Anki versions. Please check your internet connection.\n\n");
print!("{}\n\n", state.tr.launcher_unable_to_check_for_versions());
return Err(e.into());
}
};
@ -709,7 +739,7 @@ fn fetch_versions(state: &State) -> Result<Vec<String>> {
}
fn get_releases(state: &State) -> Result<Releases> {
println!("Checking for updates...");
println!("{}", state.tr.launcher_checking_for_updates());
let include_prereleases = state.prerelease_marker.exists();
let all_versions = fetch_versions(state)?;
let all_versions = filter_and_normalize_versions(all_versions, include_prereleases);
@ -911,7 +941,7 @@ fn get_anki_addons21_path() -> Result<std::path::PathBuf> {
}
fn handle_uninstall(state: &State) -> Result<bool> {
println!("Uninstall Anki's program files? (y/n)");
println!("{}", state.tr.launcher_uninstall_confirm());
print!("> ");
let _ = stdout().flush();
@ -920,7 +950,7 @@ fn handle_uninstall(state: &State) -> Result<bool> {
let input = input.trim().to_lowercase();
if input != "y" {
println!("Uninstall cancelled.");
println!("{}", state.tr.launcher_uninstall_cancelled());
println!();
return Ok(false);
}
@ -928,11 +958,11 @@ fn handle_uninstall(state: &State) -> Result<bool> {
// Remove program files
if state.uv_install_root.exists() {
anki_io::remove_dir_all(&state.uv_install_root)?;
println!("Program files removed.");
println!("{}", state.tr.launcher_program_files_removed());
}
println!();
println!("Remove all profiles/cards? (y/n)");
println!("{}", state.tr.launcher_remove_all_profiles_confirm());
print!("> ");
let _ = stdout().flush();
@ -942,7 +972,7 @@ fn handle_uninstall(state: &State) -> Result<bool> {
if input == "y" && state.anki_base_folder.exists() {
anki_io::remove_dir_all(&state.anki_base_folder)?;
println!("User data removed.");
println!("{}", state.tr.launcher_user_data_removed());
}
println!();
@ -1036,9 +1066,9 @@ fn get_mirror_urls(state: &State) -> Result<Option<(String, String)>> {
fn show_mirror_submenu(state: &State) -> Result<()> {
loop {
println!("Download mirror options:");
println!("1) No mirror");
println!("2) China");
println!("{}", state.tr.launcher_download_mirror_options());
println!("1) {}", state.tr.launcher_mirror_no_mirror());
println!("2) {}", state.tr.launcher_mirror_china());
print!("> ");
let _ = stdout().flush();
@ -1052,14 +1082,14 @@ fn show_mirror_submenu(state: &State) -> Result<()> {
if state.mirror_path.exists() {
let _ = remove_file(&state.mirror_path);
}
println!("Mirror disabled.");
println!("{}", state.tr.launcher_mirror_disabled());
break;
}
"2" => {
// Write China mirror URLs
let china_mirrors = "https://registry.npmmirror.com/-/binary/python-build-standalone/\nhttps://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple/";
write_file(&state.mirror_path, china_mirrors)?;
println!("China mirror enabled.");
println!("{}", state.tr.launcher_mirror_china_enabled());
break;
}
"" => {
@ -1067,7 +1097,7 @@ fn show_mirror_submenu(state: &State) -> Result<()> {
break;
}
_ => {
println!("Invalid input. Please try again.");
println!("{}", state.tr.launcher_invalid_input());
continue;
}
}

View file

@ -23,10 +23,10 @@ use write_strings::write_strings;
fn main() -> Result<()> {
// generate our own requirements
let map = get_ftl_data();
let mut map = get_ftl_data();
check(&map);
let modules = get_modules(&map);
write_strings(&map, &modules);
let mut modules = get_modules(&map);
write_strings(&map, &modules, "strings.rs");
typescript::write_ts_interface(&modules)?;
python::write_py_interface(&modules)?;
@ -41,5 +41,15 @@ fn main() -> Result<()> {
write_file_if_changed(path, meta_json)?;
}
}
// allow passing in "--cfg launcher" for the launcher
println!("cargo::rustc-check-cfg=cfg(launcher)");
// generate strings for the launcher
map.iter_mut()
.for_each(|(_, modules)| modules.retain(|module, _| module == "launcher"));
modules.retain(|module| module.name == "launcher");
write_strings(&map, &modules, "strings_launcher.rs");
Ok(())
}

View file

@ -5,4 +5,8 @@
#![allow(clippy::all)]
#[cfg(not(launcher))]
include!(concat!(env!("OUT_DIR"), "/strings.rs"));
#[cfg(launcher)]
include!(concat!(env!("OUT_DIR"), "/strings_launcher.rs"));

View file

@ -15,7 +15,7 @@ use crate::extract::VariableKind;
use crate::gather::TranslationsByFile;
use crate::gather::TranslationsByLang;
pub fn write_strings(map: &TranslationsByLang, modules: &[Module]) {
pub fn write_strings(map: &TranslationsByLang, modules: &[Module], out_fn: &str) {
let mut buf = String::new();
// lang->module map
@ -28,14 +28,16 @@ pub fn write_strings(map: &TranslationsByLang, modules: &[Module]) {
write_methods(modules, &mut buf);
let dir = PathBuf::from(std::env::var("OUT_DIR").unwrap());
let path = dir.join("strings.rs");
let path = dir.join(out_fn);
fs::write(path, buf).unwrap();
}
fn write_methods(modules: &[Module], buf: &mut String) {
buf.push_str(
r#"
#[allow(unused_imports)]
use crate::{I18n,Number};
#[allow(unused_imports)]
use fluent::{FluentValue, FluentArgs};
use std::borrow::Cow;