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(()) }