From 3b18097550fd5441473c91243a7c687a55230356 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Sat, 12 Jul 2025 23:34:09 +0700 Subject: [PATCH] Support user pyproject modifications again This changes 'keep existing version' to 'sync project changes' when changes to the pyproject.toml file have been detected that are newer than the installer's version. Also adds a way to temporarily enable the launcher, as we needed some other trigger than the pyproject.toml file anyway, and this approach also solves #4165. And removes the 'quit' option, since it's an uncommon operation, and the user can just close the window instead. Short-term caveat: users with older launchers/addon will trigger the old pyproject.toml mtime bump, leading to a 'sync project changes' message that will not make much sense to a typical user. --- qt/aqt/package.py | 9 ++---- qt/launcher/addon/__init__.py | 9 ++---- qt/launcher/src/main.rs | 53 ++++++++++++++++++++++++----------- 3 files changed, 42 insertions(+), 29 deletions(-) diff --git a/qt/aqt/package.py b/qt/aqt/package.py index 968218741..c8d481312 100644 --- a/qt/aqt/package.py +++ b/qt/aqt/package.py @@ -124,17 +124,14 @@ def launcher_executable() -> str | None: def trigger_launcher_run() -> None: - """Bump the mtime on pyproject.toml in the local data directory to trigger an update on next run.""" + """Create a trigger file to request launcher UI on next run.""" try: root = launcher_root() if not root: return - pyproject_path = Path(root) / "pyproject.toml" - - if pyproject_path.exists(): - # Touch the file to update its mtime - pyproject_path.touch() + trigger_path = Path(root) / ".want-launcher" + trigger_path.touch() except Exception as e: print(e) diff --git a/qt/launcher/addon/__init__.py b/qt/launcher/addon/__init__.py index 63a2cc5a9..799406e86 100644 --- a/qt/launcher/addon/__init__.py +++ b/qt/launcher/addon/__init__.py @@ -69,17 +69,14 @@ def add_python_requirements(reqs: list[str]) -> tuple[bool, str]: def trigger_launcher_run() -> None: - """Bump the mtime on pyproject.toml in the local data directory to trigger an update on next run.""" + """Create a trigger file to request launcher UI on next run.""" try: root = launcher_root() if not root: return - pyproject_path = Path(root) / "pyproject.toml" - - if pyproject_path.exists(): - # Touch the file to update its mtime - pyproject_path.touch() + trigger_path = Path(root) / ".want-launcher" + trigger_path.touch() except Exception as e: print(e) diff --git a/qt/launcher/src/main.rs b/qt/launcher/src/main.rs index 15548d3bc..d48adaf49 100644 --- a/qt/launcher/src/main.rs +++ b/qt/launcher/src/main.rs @@ -46,6 +46,8 @@ struct State { dist_python_version_path: std::path::PathBuf, uv_lock_path: std::path::PathBuf, sync_complete_marker: std::path::PathBuf, + launcher_trigger_file: std::path::PathBuf, + pyproject_modified_by_user: bool, previous_version: Option, resources_dir: std::path::PathBuf, } @@ -70,7 +72,6 @@ pub enum MainMenuChoice { ToggleBetas, ToggleCache, Uninstall, - Quit, } fn main() { @@ -106,6 +107,8 @@ fn run() -> Result<()> { dist_python_version_path: resources_dir.join(".python-version"), uv_lock_path: uv_install_root.join("uv.lock"), sync_complete_marker: uv_install_root.join(".sync_complete"), + launcher_trigger_file: uv_install_root.join(".want-launcher"), + pyproject_modified_by_user: false, // calculated later previous_version: None, resources_dir, }; @@ -125,15 +128,15 @@ fn run() -> Result<()> { &state.user_python_version_path, )?; - let pyproject_has_changed = !state.sync_complete_marker.exists() || { - let pyproject_toml_time = modified_time(&state.user_pyproject_path)?; - let sync_complete_time = modified_time(&state.sync_complete_marker)?; - Ok::(pyproject_toml_time > sync_complete_time) - } - .unwrap_or(true); + let launcher_requested = state.launcher_trigger_file.exists(); - if !pyproject_has_changed { - // If venv is already up to date, launch Anki normally + // Calculate whether user has custom edits that need syncing + let pyproject_time = file_timestamp_secs(&state.user_pyproject_path); + let sync_time = file_timestamp_secs(&state.sync_complete_marker); + state.pyproject_modified_by_user = pyproject_time > sync_time; + let pyproject_has_changed = state.pyproject_modified_by_user; + if !launcher_requested && !pyproject_has_changed { + // If no launcher request and venv is already up to date, launch Anki normally let args: Vec = std::env::args().skip(1).collect(); let cmd = build_python_command(&state, &args)?; launch_anki_normally(cmd)?; @@ -143,6 +146,11 @@ fn run() -> Result<()> { // If we weren't in a terminal, respawn ourselves in one ensure_terminal_shown()?; + if launcher_requested { + // Remove the trigger file to make request ephemeral + let _ = remove_file(&state.launcher_trigger_file); + } + print!("\x1B[2J\x1B[H"); // Clear screen and move cursor to top println!("\x1B[1mAnki Launcher\x1B[0m\n"); @@ -313,9 +321,11 @@ fn main_menu_loop(state: &State) -> Result<()> { let menu_choice = get_main_menu_choice(state)?; match menu_choice { - MainMenuChoice::Quit => std::process::exit(0), MainMenuChoice::KeepExisting => { - // Skip sync, just launch existing installation + if state.pyproject_modified_by_user { + // User has custom edits, sync them + handle_version_install_or_update(state, MainMenuChoice::KeepExisting)?; + } break; } MainMenuChoice::ToggleBetas => { @@ -372,14 +382,28 @@ fn write_sync_marker(sync_complete_marker: &std::path::Path) -> Result<()> { Ok(()) } +/// Get mtime of provided file, or 0 if unavailable +fn file_timestamp_secs(path: &std::path::Path) -> i64 { + modified_time(path) + .map(|t| t.duration_since(UNIX_EPOCH).unwrap_or_default().as_secs() as i64) + .unwrap_or_default() +} + fn get_main_menu_choice(state: &State) -> Result { loop { println!("1) Latest Anki (press Enter)"); println!("2) Choose a version"); + if let Some(current_version) = &state.current_version { let normalized_current = normalize_version(current_version); - println!("3) Keep existing version ({normalized_current})"); + + if state.pyproject_modified_by_user { + println!("3) Sync project changes"); + } else { + println!("3) 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); @@ -400,7 +424,6 @@ fn get_main_menu_choice(state: &State) -> Result { ); println!(); println!("7) Uninstall"); - println!("8) Quit"); print!("> "); let _ = stdout().flush(); @@ -440,7 +463,6 @@ fn get_main_menu_choice(state: &State) -> Result { "5" => MainMenuChoice::ToggleBetas, "6" => MainMenuChoice::ToggleCache, "7" => MainMenuChoice::Uninstall, - "8" => MainMenuChoice::Quit, _ => { println!("Invalid input. Please try again."); continue; @@ -698,9 +720,6 @@ fn update_pyproject_for_version(menu_choice: MainMenuChoice, state: &State) -> R MainMenuChoice::Version(version_kind) => { apply_version_kind(&version_kind, state)?; } - MainMenuChoice::Quit => { - std::process::exit(0); - } } Ok(()) }