diff --git a/CHANGELOG.md b/CHANGELOG.md index 52b3878..8808bb9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,9 @@ All notable changes to NotAlterra are documented in this file. rustc 1.95 nightly. - **`KNOWN_ISSUES.md` moved from `docs/` to project root** — reflects v0.3.0 privacy improvements (manual path entry live, discovery deprecated). +- **`Locate save files` menu item removed** — the deprecated auto-discovery + entry point is gone. `Set save folder` is the only path for configuring the + save location. ### Notes - Working copy diverged from remote after signing the previous commit diff --git a/GOVERNANCE.md b/GOVERNANCE.md index e0cf15a..b34e030 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -75,7 +75,7 @@ Planned changes for upcoming releases, ordered by priority. | Target | Item | |--------|------| -| v0.4.0 | Remove `discovery.rs` entirely | +| v0.4.0 | Extract `validate_custom_path` and `derive_ini_path` from `discovery.rs`, then remove the module | | v0.4.0 | Replace `disclaimer_accepted` with 0-byte sentinel file | | v0.4.0 | Drop `save_scan` from `config.ini` | | v0.4.0 | Remove `config.ini` entirely — re-request path each session | diff --git a/README.md b/README.md index c7310a3..31b1ddb 100644 --- a/README.md +++ b/README.md @@ -85,14 +85,13 @@ path manually (paste is supported). The menu is keyboard-driven: ### Menu 1. **Set save folder** — enter your save folder path manually (paste supported) -2. **Locate save files (deprecated)** — auto-scan for save folders (will be removed in v0.3.0) -3. **Recover save file** — pick a backup, preview metadata, overwrite the live save -4. **Create full backup** — copies all `savegame_*` files to `NotAlterra_Backups` -5. **Restore full backup** — overwrite the save folder from a previous backup -6. **Inspect save files** — view all GVAS properties of any `.sav` / `.bak` -7. **Manage UE5 Config (.ini) files** — backup, restore, or delete `.ini` files -8. **View disclaimer** -9. **Exit** +2. **Recover save file** — pick a backup, preview metadata, overwrite the live save +3. **Create full backup** — copies all `savegame_*` files to `NotAlterra_Backups` +4. **Restore full backup** — overwrite the save folder from a previous backup +5. **Inspect save files** — view all GVAS properties of any `.sav` / `.bak` +6. **Manage UE5 Config (.ini) files** — backup, restore, or delete `.ini` files +7. **View disclaimer** +8. **Exit** ## Where Files Live diff --git a/src/main.rs b/src/main.rs index 9e8bce7..75b1f49 100644 --- a/src/main.rs +++ b/src/main.rs @@ -76,7 +76,6 @@ struct App { log_path: PathBuf, save_folder: Option, tui_state: tui::AppState, - discovery_deprecation_shown: bool, } impl App { @@ -100,7 +99,6 @@ impl App { log_path, save_folder, tui_state, - discovery_deprecation_shown: false, }) } @@ -170,9 +168,6 @@ fn run_app(terminal: &mut Terminal) -> Result<()> { } } - // Load cached path only — no auto-scan (privacy: discovery scans user profiles) - let save_found = app.save_folder.is_some(); - // Main menu loop let mut menu_state = ListState::default().with_selected(Some(0)); @@ -184,10 +179,10 @@ fn run_app(terminal: &mut Terminal) -> Result<()> { draw_too_small(f); return; } - tui::draw_main_menu(f, &mut menu_state, &app.tui_state, save_found); + tui::draw_main_menu(f, &mut menu_state, &app.tui_state); })?; - let max_idx = 8usize; + let max_idx = 7usize; if event::poll(std::time::Duration::from_millis(250))? { if let Event::Key(key) = event::read()? { if key.kind == KeyEventKind::Release { continue; } @@ -204,18 +199,17 @@ fn run_app(terminal: &mut Terminal) -> Result<()> { let idx = menu_state.selected().unwrap_or(0); match idx { 0 => action_set_save_folder(terminal, &mut app)?, - 1 => action_locate_saves(terminal, &mut app)?, - 2 => action_recover_bak(terminal, &mut app)?, - 3 => action_create_backup(terminal, &mut app)?, - 4 => action_restore_backup(terminal, &mut app)?, - 5 => action_inspect_saves(terminal, &mut app)?, - 6 => run_ini_submenu(terminal, &mut app)?, - 7 => { + 1 => action_recover_bak(terminal, &mut app)?, + 2 => action_create_backup(terminal, &mut app)?, + 3 => action_restore_backup(terminal, &mut app)?, + 4 => action_inspect_saves(terminal, &mut app)?, + 5 => run_ini_submenu(terminal, &mut app)?, + 6 => { if let Some(false) = run_disclaimer(terminal, &mut app)? { return Ok(()); } } - 8 => return Ok(()), // Exit + 7 => return Ok(()), // Exit _ => {} } app.clear_status(); @@ -298,7 +292,12 @@ fn action_set_save_folder(terminal: &mut Terminal, app: &mut App) match key.code { KeyCode::Enter => { if ok_selected && !input_state.input.is_empty() { - let candidate = discovery::validate_custom_path(&input_state.input); + // Sanitize: strip control characters to prevent + // config.ini injection and log forgery. + let sanitized: String = input_state.input.chars() + .filter(|c| !c.is_control()) + .collect(); + let candidate = discovery::validate_custom_path(&sanitized); if let Some(path) = candidate { app.save_folder = Some(path.clone()); app.config.save_path = Some(path.display().to_string()); @@ -361,121 +360,6 @@ fn action_set_save_folder(terminal: &mut Terminal, app: &mut App) } } -/// Caches the first match as `save_path` in config.ini. -fn action_locate_saves(terminal: &mut Terminal, app: &mut App) -> Result<()> { - // Deprecation notice — show once per session - if !app.discovery_deprecation_shown { - app.discovery_deprecation_shown = true; - ok_dialog(terminal, app, - "Deprecation Notice", - "Auto-scan for save folders is deprecated and will\n\ - be removed in a future release.\n\ - \n\ - Please use 'Set save folder' from the main menu\n\ - to enter your save path manually. This protects\n\ - your privacy by not scanning other user profiles\n\ - and system directories.", - )?; - // Let the user decide — proceed with the scan anyway - } - // Run scan on a background thread so we can show a live elapsed timer. - let (tx, rx) = std::sync::mpsc::channel(); - let scan_start = std::time::Instant::now(); - std::thread::spawn(move || { - let result = discovery::discover_save_folders(); - tx.send(result).ok(); - }); - - app.set_spinner(true); - let folders = loop { - let elapsed = scan_start.elapsed().as_secs(); - terminal.draw(|f| { - tui::draw_text_screen( - f, - &app.tui_state, - &[ - Line::from(Span::styled( - "Scanning for Subnautica save files…", - Style::default().add_modifier(Modifier::BOLD), - )), - Line::from(""), - Line::from(Span::styled( - "The first scan checks every user profile and common", - Style::default().fg(Color::DarkGray), - )), - Line::from(Span::styled( - "install location — it may take a moment.", - Style::default().fg(Color::DarkGray), - )), - Line::from(""), - Line::from(Span::styled( - format!(" ⏱ {elapsed}s elapsed"), - Style::default().fg(Color::Cyan), - )), - ], - "", - ); - })?; - - match rx.try_recv() { - Ok(f) => break f, - Err(std::sync::mpsc::TryRecvError::Disconnected) => break Vec::new(), - _ => {} - } - std::thread::sleep(std::time::Duration::from_millis(100)); - }; - app.set_spinner(false); - - let elapsed = scan_start.elapsed(); - let elapsed_str = if elapsed.as_secs() >= 1 { - format!("{:.1}s", elapsed.as_secs_f64()) - } else { - format!("{}ms", elapsed.as_millis()) - }; - - if folders.is_empty() { - app.set_status("No Subnautica 2 save folders detected.", tui::StatusStyle::Error); - wait_for_key(terminal, app)?; - return Ok(()); - } - - // Cache the first result - if let Some(first) = folders.first() { - app.save_folder = Some(first.path.clone()); - app.config.save_path = Some(first.path.display().to_string()); - app.config.save_scan = Some(chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string()); - crate::config::save_config(&app.ini_path, &app.config)?; - } - - refresh_stats(&mut app.tui_state, app.save_folder.as_deref()); - - let msg = format!("Found {} save folder(s) in {elapsed_str}.", folders.len()); - app.set_status(&msg, tui::StatusStyle::Success); - - // Show found folders - let lines: Vec = std::iter::once(Line::from(Span::styled( - format!("Found {} save folder(s):", folders.len()), - Style::default().fg(Color::Green), - ))) - .chain(folders.iter().map(|f| { - Line::from(Span::styled( - format!(" {} — {}", f.label, f.path.display()), - Style::default().fg(Color::Gray), - )) - })) - .collect(); - - let prompt = "Press any key to return to menu…"; - loop { - terminal.draw(|f| tui::draw_text_screen(f, &app.tui_state, &lines, prompt))?; - if let Event::Key(_) = event::read()? { - break; - } - } - - Ok(()) -} - /// Recover a .sav from its .bak backup with a rollback safety net. fn action_recover_bak(terminal: &mut Terminal, app: &mut App) -> Result<()> { let save_folder = ensure_save_folder(terminal, app)?; diff --git a/src/tui.rs b/src/tui.rs index a91c9a5..5017b69 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -76,10 +76,9 @@ impl Default for AppState { // ── public rendering entry points ────────────────────────────────────────── /// Draw the main menu. -pub fn draw_main_menu(f: &mut Frame, state: &mut ListState, app: &AppState, _save_found: bool) { +pub fn draw_main_menu(f: &mut Frame, state: &mut ListState, app: &AppState) { let items: Vec<&str> = vec![ " Set save folder", - " Locate save files (deprecated)", " Recover save file", " Create full backup", " Restore full backup", @@ -90,7 +89,6 @@ pub fn draw_main_menu(f: &mut Frame, state: &mut ListState, app: &AppState, _sav ]; let descs: Vec<&str> = vec![ "Enter your save folder path manually", - "Auto-scan for save folders (deprecated — will be removed in a future release)", "Restore a save file from a backup", "Copy the savegame files to NotAlterra_Backups", "Restore a full backup from NotAlterra_Backups", @@ -797,6 +795,10 @@ pub fn draw_input_dialog( Paragraph::new(buttons).alignment(Alignment::Center), Rect { y: inner.y + 6, height: 1, width: inner.width, x: inner.x }, ); + + // Whale + let bar = Rect { x: 0, y: f.area().height.saturating_sub(1), width: f.area().width, height: 1 }; + draw_whale_separator(f, bar, _app); } /// Truncate a path to show the tail (most specific directories).