doc comments, governance, decisions, polish

This commit is contained in:
2026-06-02 23:49:44 +02:00
parent 8ffd5c2ce4
commit 11c5eeee3f
7 changed files with 207 additions and 43 deletions
+22 -9
View File
@@ -13,8 +13,11 @@ use std::path::{Path, PathBuf};
// ── process detection ──────────────────────────────────────────────────────
/// Return `true` if Subnautica 2 appears to be running.
/// Check if Subnautica 2 is currently running (Windows).
/// Process detection is disabled — returns `false` to avoid AV false positives.
///
/// Always returns `false` — process detection (`tasklist` / `pgrep`) is
/// intentionally disabled to avoid Windows Defender false positives
/// (Trojan:Win32/Wacatac.C!ml). Users are reminded to close the game
/// manually before using the tool.
#[cfg(target_os = "windows")]
pub fn game_running() -> bool {
// Process detection disabled — avoid AV false-positives.
@@ -34,7 +37,9 @@ fn _game_running_windows() -> bool {
}
/// Check if Subnautica 2 is currently running (Linux).
/// Process detection is disabled — always returns `false` to avoid AV false positives.
///
/// Always returns `false` — process detection via `pgrep` is disabled to
/// avoid false positives. Users are reminded to close the game manually.
#[cfg(not(target_os = "windows"))]
pub fn game_running() -> bool {
false
@@ -61,7 +66,8 @@ fn _game_running_linux() -> bool {
// ── transaction logging ────────────────────────────────────────────────────
/// Path to transaction.log next to the binary.
/// Path to `transaction.log` alongside the binary. All timestamped actions
/// are appended here for audit trail purposes.
pub fn log_path() -> PathBuf {
exe_dir().join("transaction.log")
}
@@ -77,7 +83,11 @@ fn exe_dir() -> PathBuf {
/// Maximum lines before rotation.
const MAX_LOG_LINES: usize = 10_000;
/// Append a timestamped log entry. Auto-rotates if the log exceeds 10k lines.
/// Append a timestamped log entry to `transaction.log`.
///
/// Format: `YYYY-MM-DD HH:MM:SS | ACTION | detail | result`
/// Auto-rotates if the log exceeds 10,000 lines — the oldest lines are
/// discarded, keeping only the most recent 10,000.
pub fn log_action(
action: &str,
detail: &str,
@@ -105,9 +115,11 @@ pub fn log_action(
f.write_all(line.as_bytes())?;
Ok(())
}
/// Truncate a filesystem path to start at `Subnautica2/` or `Subnautica2\`
/// for privacy-safe logging. Returns the original path if no truncation
/// is possible.
/// Truncate a filesystem path to start at `Subnautica2/` or `Subnautica2\`,
/// stripping the user-specific prefix for privacy-safe logging.
///
/// Returns the original path unchanged if `Subnautica2` is not found in the
/// input (e.g. custom paths entered via `Set save folder`).
pub fn sanitize_path(p: &str) -> String {
let needle = "Subnautica2";
let sep = if p.contains('\\') { "\\" } else { "/" };
@@ -118,7 +130,8 @@ pub fn sanitize_path(p: &str) -> String {
}
}
/// Check whether a path looks like a network/UNC path (for warning purposes).
/// Check whether a path looks like a network/UNC path for warning purposes.
/// Matches paths starting with `\\` (Windows) or `//` (Linux).
pub fn is_network_path(p: &str) -> bool {
p.starts_with("\\\\") || p.starts_with("//")
}
+31 -17
View File
@@ -111,7 +111,9 @@ impl App {
}
}
/// Internal helper — see module-level documentation for context.
/// Return the directory containing the running executable. Used to locate
/// the sentinel file, backups directory, and `transaction.log` alongside
/// the binary.
fn exe_dir() -> PathBuf {
std::env::current_exe()
.ok()
@@ -119,7 +121,9 @@ fn exe_dir() -> PathBuf {
.unwrap_or_else(|| PathBuf::from("."))
}
/// Internal helper — see module-level documentation for context.
/// Refresh the dashboard counters (live saves, backups, ini backup status)
/// shown in the header bar. Called after changing the save folder or after
/// any backup/restore operation.
fn refresh_stats(tui_state: &mut tui::AppState, save_folder: Option<&Path>) {
tui_state.save_path = save_folder.map(|p| p.display().to_string());
let backup_root = exe_dir().join("NotAlterra_Backups");
@@ -666,7 +670,7 @@ fn action_restore_backup<B: Backend>(terminal: &mut Terminal<B>, app: &mut App)
// ── .ini submenu ───────────────────────────────────────────────────────────
/// Internal helper — see module-level documentation for context.
/// Display the .ini management submenu with Backup, Restore, and Delete options.
fn run_ini_submenu<B: Backend>(terminal: &mut Terminal<B>, app: &mut App) -> Result<()> {
let ini_path = get_ini_path(terminal, app)?;
let backup_root = app.backup_root();
@@ -719,7 +723,7 @@ fn run_ini_submenu<B: Backend>(terminal: &mut Terminal<B>, app: &mut App) -> Res
Ok(())
}
/// Internal helper — see module-level documentation for context.
/// Back up all `.ini` files from the Config\Windows folder into a timestamped archive.
fn ini_backup_action<B: Backend>(
terminal: &mut Terminal<B>,
app: &mut App,
@@ -747,7 +751,8 @@ fn ini_backup_action<B: Backend>(
Ok(())
}
/// Internal helper — see module-level documentation for context.
/// Restore `.ini` files from a selected backup into the Config\Windows folder.
/// Creates a pre-restore safety copy of the current `.ini` files first.
fn ini_restore_action<B: Backend>(
terminal: &mut Terminal<B>,
app: &mut App,
@@ -811,7 +816,8 @@ fn ini_restore_action<B: Backend>(
Ok(())
}
/// Internal helper — see module-level documentation for context.
/// Delete all `.ini` files from the Config\Windows folder.
/// Refuses to proceed unless at least one `.ini` backup exists in the backup root.
fn ini_delete_action<B: Backend>(
terminal: &mut Terminal<B>,
app: &mut App,
@@ -956,7 +962,9 @@ fn action_inspect_saves<B: Backend>(terminal: &mut Terminal<B>, app: &mut App) -
// ── helpers ────────────────────────────────────────────────────────────────
/// Internal helper see module-level documentation for context.
/// Poll for a single key event with a timeout in milliseconds.
/// Returns `None` if no key is pressed within the timeout. Filters out
/// `KeyEventKind::Release` events to avoid double-firing on held keys.
fn poll_key(timeout_ms: u64) -> Result<Option<crossterm::event::KeyEvent>> {
if event::poll(std::time::Duration::from_millis(timeout_ms))? {
if let Event::Key(key) = event::read()? {
@@ -999,10 +1007,9 @@ fn slot_number(slot: &str) -> String {
.unwrap_or_else(|| slot.to_string())
}
/// Internal helper — see module-level documentation for context.
/// Internal helper — see module-level documentation for context.
/// Display an informational dialog with a single OK button.
/// Display a dialog with styled content lines (colors, bold).
/// Display a dialog with styled content lines (colors, bold). Accepts
/// `Line` slicesuse for metadata displays, help text, or any content
/// that needs inline formatting. Press Enter or Space to dismiss.
fn ok_dialog_styled<B: Backend>(terminal: &mut Terminal<B>, app: &App, title: &str, lines: &[Line]) -> Result<()> {
loop {
terminal.draw(|f| tui::draw_ok_dialog_styled(f, &app.tui_state, title, lines))?;
@@ -1014,8 +1021,9 @@ fn ok_dialog_styled<B: Backend>(terminal: &mut Terminal<B>, app: &App, title: &s
}
}
/// Internal helper — see module-level documentation for context.
/// Display an informational dialog with a single OK button.
/// Display a plain-text informational dialog with a single OK button.
/// `msg` supports newlines for multi-line messages. Press Enter or
/// Space to dismiss. For styled content, use `ok_dialog_styled`.
fn ok_dialog<B: Backend>(terminal: &mut Terminal<B>, app: &App, title: &str, msg: &str) -> Result<()> {
loop {
terminal.draw(|f| tui::draw_ok_dialog(f, &app.tui_state, title, msg))?;
@@ -1027,13 +1035,15 @@ fn ok_dialog<B: Backend>(terminal: &mut Terminal<B>, app: &App, title: &str, msg
}
}
/// Internal helper — see module-level documentation for context.
/// Check whether at least one backup directory exists in the backup root.
/// Used to gate destructive operations behind a backup requirement.
fn has_existing_backup(app: &App) -> bool {
let root = app.backup_root();
root.exists() && std::fs::read_dir(&root).is_ok_and(|mut d| d.any(|e| e.is_ok_and(|e| e.path().is_dir())))
}
/// Internal helper — see module-level documentation for context.
/// Gate: warn the user if no full backup exists yet. Returns `false` if
/// no backup is found (caller should abort the destructive operation).
fn require_backup<B: Backend>(terminal: &mut Terminal<B>, app: &mut App) -> Result<bool> {
if has_existing_backup(app) {
return Ok(true);
@@ -1043,7 +1053,9 @@ fn require_backup<B: Backend>(terminal: &mut Terminal<B>, app: &mut App) -> Resu
Ok(false)
}
/// Internal helper — see module-level documentation for context.
/// Derive the canonical `.sav` target filename from a `.bak` filename.
/// E.g. `savegame_0_9.bak` → `savegame_0.sav`. Falls back to replacing
/// `.bak` with `.sav` if the slot pattern is not recognized.
fn derive_target_sav(bak_name: &str) -> String {
crate::gvas::derive_slot_from_filename(bak_name)
.map(|s| format!("{s}.sav"))
@@ -1150,7 +1162,9 @@ fn fs_meta(path: &Path) -> Result<std::fs::Metadata, ()> {
std::fs::metadata(path).map_err(|_| ())
}
/// Internal helper — see module-level documentation for context.
/// Format a playtime value in seconds to a human-readable string
/// (`HHh MMm` or `MMm`, zero-padded to 2 digits). Returns `—` for
/// `None` or sub-minute values.
fn format_playtime(seconds: Option<f64>) -> String {
match seconds {
Some(s) if s >= 3600.0 => {
+20 -8
View File
@@ -209,8 +209,9 @@ pub fn draw_confirm_popup(
draw_whale_separator(f, bar, app);
}
/// Render a simple informational dialog with a message.
/// Shows title, body text, and an OK button.
/// Render an informational dialog with a plain-text message and OK button.
/// Auto-sizes to fit content. Title is displayed in cyan, message in gray,
/// whale separator at the bottom. Press Enter or Space to dismiss.
pub fn draw_ok_dialog(f: &mut Frame, app: &AppState, title: &str, message: &str) {
let content_w = message.lines().map(|l| l.len()).max().unwrap_or(20).max(title.len()) as u16 + 10;
let popup_w = content_w.max(50).min(f.area().width.saturating_sub(4));
@@ -231,7 +232,9 @@ pub fn draw_ok_dialog(f: &mut Frame, app: &AppState, title: &str, message: &str)
draw_whale_separator(f, bar, app);
}
/// Render a dialog with styled content lines.\n/// Supports inline formatting (colors, bold) via [`Line`].
/// Render a dialog with styled content lines. Supports inline formatting
/// (colors, bold) via [`Line`] slices. Use for metadata displays, help
/// text, or any content that needs per-span styling.
pub fn draw_ok_dialog_styled(f: &mut Frame, app: &AppState, title: &str, lines: &[Line]) {
let content_w = lines.iter().map(|l| l.width() as u16).max().unwrap_or(20).max(title.len() as u16) + 10;
let popup_w = content_w.max(50).min(f.area().width.saturating_sub(4));
@@ -288,7 +291,9 @@ pub fn draw_sub_menu(
draw_status_bar(f, chunks[3], app);
}
/// Draw a simple text screen with a "press any key" prompt.
/// Draw a full-screen text display with a "press any key" prompt at the
/// bottom. Used for status messages during long operations (scanning,
/// backing up) and for displaying scan results.
pub fn draw_text_screen(
f: &mut Frame,
app: &AppState,
@@ -586,8 +591,10 @@ fn draw_status_bar(f: &mut Frame, area: Rect, app: &AppState) {
draw_whale_separator(f, area, app);
}
/// Separator line with a whale patrolling right-to-left.
/// Disappears for ~10 s after reaching the left edge.
/// Draw the bottom status bar with an animated whale patrolling right-to-left.
/// The whale moves one position every 180ms. After reaching the left edge,
/// it disappears for ~5.4s (30 cooldown ticks) before reappearing on the
/// right. Two variants alternate every 400ms.
pub fn draw_whale_separator(f: &mut Frame, area: Rect, app: &AppState) {
if area.width < 4 { return; }
let elapsed = app.whale_start.elapsed().as_millis() as u64;
@@ -801,8 +808,13 @@ pub fn draw_input_dialog(
draw_whale_separator(f, bar, _app);
}
/// Truncate a path to show the tail (most specific directories).
/// e.g. `C:\Users\...\Subnautica2\Saved\SaveGames` → `…\Subnautica2\Saved\SaveGames`
/// Truncate a filesystem path to show only the most specific directories.
/// Walks forward to the first path separator after the truncation point to
/// avoid splitting mid-component.
///
/// Examples:
/// - `C:\Users\user\AppData\...\SaveGames` → `…\AppData\...\SaveGames`
/// - A short path that fits `max_width` is returned unchanged.
fn truncate_path_tail(path: &str, max_width: usize) -> String {
if path.len() <= max_width {
return path.to_string();