use std::collections::BTreeMap;
use std::path::Path;
use serde::{Deserialize, Serialize};
use crate::paths;
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct State {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_vm: Option<String>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub vms: BTreeMap<String, VmState>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub update: Option<UpdateState>,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct VmState {
#[serde(default)]
pub gui: bool,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct UpdateState {
#[serde(default)]
pub last_checked_at: u64,
#[serde(default)]
pub last_notified_at: u64,
#[serde(default)]
pub latest_known: Option<String>,
}
impl State {
pub fn load() -> Self {
let path = paths::state_file();
if !path.exists() {
return Self::default();
}
match std::fs::read_to_string(&path) {
Ok(s) => toml::from_str(&s).unwrap_or_default(),
Err(_) => Self::default(),
}
}
pub fn save(&self) -> std::io::Result<()> {
paths::ensure_dirs()?;
let s = toml::to_string(self).expect("serialize state");
write_atomically(&paths::state_file(), &s)
}
}
fn write_atomically(path: &Path, contents: &str) -> std::io::Result<()> {
let dir = path.parent().unwrap_or_else(|| Path::new("."));
std::fs::create_dir_all(dir)?;
let tmp = dir.join(format!(".{}.tmp", path.file_name().unwrap().to_string_lossy()));
std::fs::write(&tmp, contents)?;
std::fs::rename(&tmp, path)
}
pub fn set_default(vm: &str) -> std::io::Result<()> {
let mut s = State::load();
s.default_vm = Some(vm.to_string());
s.save()
}
pub fn clear_default_if_matches(vm: &str) -> std::io::Result<()> {
let mut s = State::load();
if s.default_vm.as_deref() == Some(vm) {
s.default_vm = None;
s.save()
} else {
Ok(())
}
}
pub fn set_vm_gui(vm: &str, gui: bool) -> std::io::Result<()> {
let mut s = State::load();
s.vms.insert(vm.to_string(), VmState { gui });
s.save()
}
pub fn vm_gui(vm: &str) -> Option<bool> {
State::load().vms.get(vm).map(|v| v.gui)
}
pub fn forget_vm(vm: &str) -> std::io::Result<()> {
let mut s = State::load();
if s.vms.remove(vm).is_some() {
s.save()
} else {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
static ENV_LOCK: Mutex<()> = Mutex::new(());
fn with_temp_root<F: FnOnce()>(f: F) {
let _g = ENV_LOCK.lock().unwrap();
let tmp = tempfile::tempdir().unwrap();
let prev = std::env::var_os("RUSTA_STATE_ROOT");
std::env::set_var("RUSTA_STATE_ROOT", tmp.path());
f();
match prev {
Some(v) => std::env::set_var("RUSTA_STATE_ROOT", v),
None => std::env::remove_var("RUSTA_STATE_ROOT"),
}
}
#[test]
fn load_returns_default_when_missing() {
with_temp_root(|| {
let s = State::load();
assert!(s.default_vm.is_none());
});
}
#[test]
fn save_and_reload_roundtrip() {
with_temp_root(|| {
set_default("hello").unwrap();
let s = State::load();
assert_eq!(s.default_vm.as_deref(), Some("hello"));
});
}
#[test]
fn clear_default_only_when_match() {
with_temp_root(|| {
set_default("a").unwrap();
clear_default_if_matches("b").unwrap();
assert_eq!(State::load().default_vm.as_deref(), Some("a"));
clear_default_if_matches("a").unwrap();
assert!(State::load().default_vm.is_none());
});
}
#[test]
fn vm_gui_roundtrip() {
with_temp_root(|| {
assert_eq!(vm_gui("lab"), None);
set_vm_gui("lab", true).unwrap();
assert_eq!(vm_gui("lab"), Some(true));
set_vm_gui("lab", false).unwrap();
assert_eq!(vm_gui("lab"), Some(false));
forget_vm("lab").unwrap();
assert_eq!(vm_gui("lab"), None);
});
}
#[test]
fn old_schema_without_vms_table_still_loads() {
with_temp_root(|| {
paths::ensure_dirs().unwrap();
std::fs::write(paths::state_file(), b"default_vm = \"hello\"\n").unwrap();
let s = State::load();
assert_eq!(s.default_vm.as_deref(), Some("hello"));
assert!(s.vms.is_empty());
assert_eq!(vm_gui("hello"), None);
});
}
#[test]
fn forget_vm_is_noop_when_absent() {
with_temp_root(|| {
forget_vm("missing").unwrap();
assert!(State::load().vms.is_empty());
});
}
#[test]
fn load_corrupt_file_returns_default() {
with_temp_root(|| {
paths::ensure_dirs().unwrap();
std::fs::write(paths::state_file(), b"@@@not toml@@@").unwrap();
let s = State::load();
assert!(s.default_vm.is_none());
});
}
}