mirror of
https://github.com/forkless/NotAlterra.git
synced 2026-08-19 09:35:12 +02:00
remove config.ini, replace disclaimer with sentinel file
This commit is contained in:
@@ -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
|
||||
|
||||
Generated
+1
-1
@@ -495,7 +495,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "notalterra"
|
||||
version = "0.3.1"
|
||||
version = "0.3.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "notalterra"
|
||||
version = "0.3.1"
|
||||
version = "0.3.2"
|
||||
edition = "2021"
|
||||
authors = ["NotAlterra"]
|
||||
license = "MIT"
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
+4
-18
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+22
-126
@@ -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
@@ -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'."
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user