From c09785272b53e9d2b232c4f6f0082ce7533d28ec Mon Sep 17 00:00:00 2001 From: forkless Date: Tue, 2 Jun 2026 19:47:56 +0200 Subject: [PATCH] remove config.ini, replace disclaimer with sentinel file --- CHANGELOG.md | 15 +++++ Cargo.lock | 2 +- Cargo.toml | 2 +- GOVERNANCE.md | 3 - KNOWN_ISSUES.md | 22 ++----- README.md | 28 ++++----- _release.md | 7 +++ src/config.rs | 148 +++++++----------------------------------------- src/main.rs | 46 +++------------ 9 files changed, 68 insertions(+), 205 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e911b4f..f9a13c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,21 @@ All notable changes to NotAlterra are documented in this file. --- +## [v0.3.2] — 2026-06-02 + +### Removed +- **`config.ini` eliminated entirely** — no save path, disclaimer flag, or + scan timestamp is written to disk anymore. The save folder is session-only, + entered via **Set save folder**. The disclaimer acceptance is tracked via a + 0-byte sentinel file (`NotAlterra_LICENSE_ACCEPTED`) alongside the binary. + - `src/config.rs` reduced to sentinel utilities and `exe_dir()` + - `AppConfig`, `load_config()`, `save_config()` removed + - Integration tests for config round-trips removed (replaced by sentinel test) + +### Changed +- `get_ini_path()` now derives the Config/Windows path from the save folder + at runtime — no cached `ini_path` in memory or on disk. + ## [v0.3.1] — 2026-06-02 ### Added diff --git a/Cargo.lock b/Cargo.lock index 28c5f08..7217b91 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -495,7 +495,7 @@ dependencies = [ [[package]] name = "notalterra" -version = "0.3.1" +version = "0.3.2" dependencies = [ "anyhow", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 17793fb..7e86def 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "notalterra" -version = "0.3.1" +version = "0.3.2" edition = "2021" authors = ["NotAlterra"] license = "MIT" diff --git a/GOVERNANCE.md b/GOVERNANCE.md index 495f10f..120d3c8 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -85,9 +85,6 @@ Planned changes for upcoming releases, ordered by priority. | Target | Item | |--------|------| | 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 | Items may shift between releases depending on feedback and urgency. diff --git a/KNOWN_ISSUES.md b/KNOWN_ISSUES.md index 175e06b..f347462 100644 --- a/KNOWN_ISSUES.md +++ b/KNOWN_ISSUES.md @@ -1,24 +1,10 @@ # Known Issues -## config.ini persists the save path - -`config.ini` caches the save-folder path on disk next to the binary. Paths are -sanitized in transaction logs, but the raw path remains in the config file for -re-use across sessions. - -The recommended workflow now uses **Set save folder** from the main menu to -enter paths manually. Delete `config.ini` to clear the cached path — the app -will prompt you to set a new one on next launch. - -**Planned**: Remove `config.ini` entirely in a future release. The path would -be re-requested from the user on each session. v0.4.0 target. - ## Discovery module is deprecated Auto-scan for save folders (`discovery.rs`) is deprecated in favor of the -**Set save folder** menu option. The `Locate save files` item shows a -deprecation notice once per session. Scanning user profiles and system -directories is a privacy concern and this module is scheduled for removal. +**Set save folder** menu option. The module remains for `validate_custom_path` +and `derive_ini_path` utilities. -**Planned**: Remove `discovery.rs` entirely in v0.4.0. Users will enter their -save path manually via `Set save folder`. +**Planned**: Extract the two utility functions into `config.rs`, then remove +`discovery.rs` entirely in v0.4.0. diff --git a/README.md b/README.md index 31b1ddb..af0a9ab 100644 --- a/README.md +++ b/README.md @@ -114,30 +114,22 @@ path manually (paste is supported). The menu is keyboard-driven: ... ``` -Use **Set save folder** from the menu to enter your save path. The path is -persisted in `config.ini` for re-use across sessions. Delete the config file -or use the menu option again to change it. +Use **Set save folder** from the menu to enter your save path (paste is +supported). The path exists only in memory for the current session — +re-enter it the next time you run the tool. Backups are stored in `NotAlterra_Backups\` alongside the binary. -## config.ini +## Session Persistence -Created automatically next to the binary: +No configuration file is written to disk. The save folder path exists only +in memory for the current session — set it each time via **Set save folder** +from the main menu (paste is supported). -```ini -[alterra] -save_path = C:\Users\...\Subnautica2\Saved\SaveGames -save_scan = 2026-05-31 18:00:00 -disclaimer_accepted = true -ini_path = C:\Users\...\Subnautica2\Saved\Config\Windows -``` - -Delete `config.ini` to clear the cached path — you will be prompted to set -a new one on next launch. - -Only the disclaimer flag and save-folder paths are stored — no backup -state or file metadata is persisted. +The disclaimer acceptance is tracked via a 0-byte sentinel file +(`NotAlterra_LICENSE_ACCEPTED`) alongside the binary. Delete this file to +re-prompt the disclaimer on next launch. ## Platform Support diff --git a/_release.md b/_release.md index 7578e78..8419c51 100644 --- a/_release.md +++ b/_release.md @@ -4,6 +4,13 @@ Cross-platform terminal application. No admin permissions or network access req Pre-compiled binaries — no installation, no dependencies. Just download, extract, and run. +### v0.3.2 + +• `config.ini` removed entirely — no paths written to disk +• Save folder is session-only, entered via `Set save folder` +• Disclaimer tracked via 0-byte sentinel file (`NotAlterra_LICENSE_ACCEPTED`) +• Sentinel and path utilities live in reduced `config.rs` + ### v0.3.1 • Patrolling whale added to Set save folder input dialog diff --git a/src/config.rs b/src/config.rs index 0a42c84..15251e0 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,138 +1,34 @@ -//! config.ini read/write. +//! Minimal path utilities — no persistent config. //! -//! The file sits next to the binary and stores cached paths plus the -//! disclaimer-acceptance flag. Format is a flat `[alterra]` section: -//! -//! ```ini -//! [alterra] -//! save_path = C:\Users\...\Subnautica2\Saved\SaveGames -//! save_scan = 2026-05-26 19:45:22 -//! disclaimer_accepted = true -//! ini_path = C:\Users\...\Subnautica2\Saved\Config\Windows -//! ``` +//! Save folder and Config/Windows paths are now session-only, entered +//! via the `Set save folder` menu. The disclaimer acceptance is tracked +//! via a 0-byte sentinel file (`NotAlterra_LICENSE_ACCEPTED`) next to the +//! binary instead of a config file. -use anyhow::{Context, Result}; -use std::fs; -use std::io::{BufRead, BufReader, Write}; use std::path::{Path, PathBuf}; -/// In-memory representation of the config.ini keys. -#[derive(Debug, Clone, Default)] -pub struct AppConfig { - /// Last-known save folder (SaveGames) - pub save_path: Option, - /// Last-known Config\Windows folder - pub ini_path: Option, - /// Timestamp of last successful scan - pub save_scan: Option, - /// Whether disclaimer was accepted - pub disclaimer_accepted: bool, +/// Path to the disclaimer sentinel file alongside the binary. +pub fn sentinel_path() -> PathBuf { + exe_dir().join("NotAlterra_LICENSE_ACCEPTED") } -/// Path to config.ini alongside the binary. -pub fn ini_path() -> PathBuf { - let exe_dir = std::env::current_exe() - .ok() - .and_then(|p| p.parent().map(Path::to_path_buf)) - .unwrap_or_else(|| PathBuf::from(".")); - exe_dir.join("config.ini") +/// Return `true` if the disclaimer sentinel exists. +pub fn disclaimer_accepted() -> bool { + sentinel_path().exists() } -/// Read and parse config.ini. -pub fn load_config(path: &Path) -> Result { - let mut cfg = AppConfig::default(); - - if !path.exists() { - return Ok(cfg); - } - - let f = fs::File::open(path) - .with_context(|| format!("cannot open {}", path.display()))?; - let reader = BufReader::new(f); - - for line in reader.lines() { - let line = line?; - let trimmed = line.trim(); - - if trimmed.is_empty() || trimmed.starts_with('[') { - continue; - } - - if let Some(val) = trimmed.strip_prefix("save_path").and_then(strip_eq) { - cfg.save_path = Some(val.to_string()); - } else if let Some(val) = trimmed.strip_prefix("ini_path").and_then(strip_eq) { - cfg.ini_path = Some(val.to_string()); - } else if let Some(val) = trimmed.strip_prefix("save_scan").and_then(strip_eq) { - cfg.save_scan = Some(val.to_string()); - } else if let Some(val) = trimmed.strip_prefix("disclaimer_accepted").and_then(strip_eq) { - cfg.disclaimer_accepted = val.trim() == "true"; - } - } - - Ok(cfg) -} - -/// Strip `=` and surrounding whitespace. Returns `Some(value)`. -fn strip_eq(s: &str) -> Option<&str> { - Some(s.trim().strip_prefix('=')?.trim()) -} - -/// Write config back to disk, preserving non-managed keys. -pub fn save_config(path: &Path, cfg: &AppConfig) -> Result<()> { - // Preserve unknown keys by reading the old file first. - let preserved = preserved_keys(path).unwrap_or_default(); - - if let Some(parent) = path.parent() { - fs::create_dir_all(parent).ok(); - } - - let mut f = fs::File::create(path) - .with_context(|| format!("cannot write {}", path.display()))?; - - writeln!(f, "[alterra]")?; - - if let Some(ref lp) = cfg.save_path { - writeln!(f, "save_path = {lp}")?; - } - if let Some(ref cp) = cfg.ini_path { - writeln!(f, "ini_path = {cp}")?; - } - if let Some(ref ls) = cfg.save_scan { - writeln!(f, "save_scan = {ls}")?; - } - writeln!( - f, - "disclaimer_accepted = {}", - cfg.disclaimer_accepted - )?; - - // Append any extra keys we didn't touch - for (k, v) in &preserved { - writeln!(f, "{k} = {v}")?; - } - +/// Create the disclaimer sentinel (0-byte file). +pub fn accept_disclaimer() -> std::io::Result<()> { + std::fs::write(sentinel_path(), [])?; Ok(()) } -/// Collect key/value pairs from the old config that this tool doesn't own. -fn preserved_keys(path: &Path) -> Result> { - let known = &["save_path", "ini_path", "save_scan", "disclaimer_accepted"]; - let mut out = Vec::new(); - - let f = fs::File::open(path)?; - for line in BufReader::new(f).lines() { - let line = line?; - let trimmed = line.trim(); - if trimmed.is_empty() || trimmed.starts_with('[') { - continue; - } - if let Some(eq) = trimmed.find('=') { - let key = trimmed[..eq].trim(); - if !known.contains(&key) { - let val = trimmed[eq + 1..].trim(); - out.push((key.to_string(), val.to_string())); - } - } - } - Ok(out) +/// Return the directory containing the running executable. +pub fn exe_dir() -> PathBuf { + std::env::current_exe() + .ok() + .and_then(|p| p.parent().map(Path::to_path_buf)) + .unwrap_or_else(|| PathBuf::from(".")) } + + diff --git a/src/main.rs b/src/main.rs index 75b1f49..7a9b45e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,7 +18,6 @@ mod tui; use anyhow::Result; use chrono::TimeZone; -use config::AppConfig; use crossterm::{ event::{self, DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture, Event, KeyCode, KeyEventKind}, execute, @@ -71,8 +70,6 @@ fn main() -> Result<()> { // ── app state ────────────────────────────────────────────────────────────── struct App { - config: AppConfig, - ini_path: PathBuf, log_path: PathBuf, save_folder: Option, tui_state: tui::AppState, @@ -80,24 +77,11 @@ struct App { impl App { fn new() -> Result { - let ini_path = crate::config::ini_path(); - let config = crate::config::load_config(&ini_path)?; let log_path = guard::log_path(); - - let save_folder = config.save_path.as_deref().map(PathBuf::from); - - let mut tui_state = tui::AppState { version: VERSION.to_string(), ..Default::default() }; - - tui_state.save_path = save_folder.as_ref().map(|p| p.display().to_string()); - - // Refresh stats for the dashboard - refresh_stats(&mut tui_state, save_folder.as_deref()); - + let tui_state = tui::AppState { version: VERSION.to_string(), ..Default::default() }; Ok(Self { - config, - ini_path, log_path, - save_folder, + save_folder: None, tui_state, }) } @@ -161,7 +145,7 @@ fn run_app(terminal: &mut Terminal) -> Result<()> { )?; // Disclaimer flow - if !app.config.disclaimer_accepted { + if !crate::config::disclaimer_accepted() { match run_disclaimer(terminal, &mut app)? { Some(true) => {} // accepted _ => return Ok(()), // declined or cancelled on first launch → exit @@ -242,14 +226,11 @@ fn run_disclaimer(terminal: &mut Terminal, app: &mut App) -> Resu KeyCode::Left | KeyCode::Right | KeyCode::Up | KeyCode::Down => selected_yes = !selected_yes, KeyCode::Char('y') | KeyCode::Char('Y') => { guard::log_action("LICENSE", "accepted", "OK", &app.log_path)?; - app.config.disclaimer_accepted = true; - crate::config::save_config(&app.ini_path, &app.config)?; + crate::config::accept_disclaimer()?; return Ok(Some(true)); } KeyCode::Char('n') | KeyCode::Char('N') => { guard::log_action("LICENSE", "declined", "OK", &app.log_path)?; - app.config.disclaimer_accepted = false; - crate::config::save_config(&app.ini_path, &app.config)?; return Ok(Some(false)); } KeyCode::Esc => { @@ -259,8 +240,9 @@ fn run_disclaimer(terminal: &mut Terminal, app: &mut App) -> Resu let accepted = selected_yes; let detail = if accepted { "accepted" } else { "declined" }; guard::log_action("LICENSE", detail, "OK", &app.log_path)?; - app.config.disclaimer_accepted = accepted; - crate::config::save_config(&app.ini_path, &app.config)?; + if accepted { + crate::config::accept_disclaimer()?; + } return Ok(Some(accepted)); } _ => {} @@ -300,8 +282,6 @@ fn action_set_save_folder(terminal: &mut Terminal, app: &mut App) 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()); - crate::config::save_config(&app.ini_path, &app.config)?; refresh_stats(&mut app.tui_state, app.save_folder.as_deref()); let msg = format!("Save folder set to {}", path.display()); app.set_status(&msg, tui::StatusStyle::Success); @@ -992,23 +972,13 @@ fn ensure_save_folder(_terminal: &mut Terminal, app: &mut App) -> ) } -/// Derive the Config\Windows path from the save folder or cached config. +/// Derive the Config\Windows path from the save folder. fn get_ini_path(_terminal: &mut Terminal, app: &mut App) -> Result { - // Try cached config path - if let Some(ref cp) = app.config.ini_path { - let p = PathBuf::from(cp); - if p.exists() { - return Ok(p); - } - } - - // Derive from save folder if let Some(ref sf) = app.save_folder { if let Some(cp) = discovery::derive_ini_path(sf) { return Ok(cp); } } - anyhow::bail!( "Cannot determine Config/Windows path. Set your save folder first via 'Set save folder'." )