remove config.ini, replace disclaimer with sentinel file

This commit is contained in:
2026-06-02 19:48:34 +02:00
parent 7386ecb67a
commit c09785272b
9 changed files with 68 additions and 205 deletions
+22 -126
View File
@@ -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<String>,
/// Last-known Config\Windows folder
pub ini_path: Option<String>,
/// Timestamp of last successful scan
pub save_scan: Option<String>,
/// 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<AppConfig> {
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<Vec<(String, String)>> {
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("."))
}
+8 -38
View File
@@ -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<PathBuf>,
tui_state: tui::AppState,
@@ -80,24 +77,11 @@ struct App {
impl App {
fn new() -> Result<Self> {
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<B: Backend>(terminal: &mut Terminal<B>) -> 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<B: Backend>(terminal: &mut Terminal<B>, 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<B: Backend>(terminal: &mut Terminal<B>, 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<B: Backend>(terminal: &mut Terminal<B>, 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<B: Backend>(_terminal: &mut Terminal<B>, 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<B: Backend>(_terminal: &mut Terminal<B>, app: &mut App) -> Result<PathBuf> {
// 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'."
)