use std::collections::HashMap;
use std::fs;
use std::path::Path;
use std::path::PathBuf;
use bevy::prelude::*;
use super::WindowKey;
use super::constants::STATE_FILE;
use super::state_format;
use super::types::WindowState;
pub(crate) fn get_default_state_path() -> Option<PathBuf> {
let exe = std::env::current_exe().ok()?;
let exe_name = exe.file_stem()?.to_str()?;
let is_cargo_example = exe.parent().and_then(|p| p.file_name()) == Some("examples".as_ref());
if is_cargo_example {
dirs::config_dir().map(|d| {
d.join(env!("CARGO_PKG_NAME"))
.join(format!("{exe_name}.ron"))
})
} else {
dirs::config_dir().map(|d| d.join(exe_name).join(STATE_FILE))
}
}
pub(crate) fn get_state_path_for_app(app_name: &str) -> Option<PathBuf> {
dirs::config_dir().map(|d| d.join(app_name).join(STATE_FILE))
}
pub(crate) fn load_all_states(path: &Path) -> Option<HashMap<WindowKey, WindowState>> {
let contents = fs::read_to_string(path).ok()?;
state_format::decode(&contents)
}
pub(crate) fn save_all_states(path: &Path, states: &HashMap<WindowKey, WindowState>) {
if let Some(parent) = path.parent()
&& let Err(e) = fs::create_dir_all(parent)
{
warn!("[save_all_states] Failed to create directory {parent:?}: {e}");
return;
}
match state_format::encode(states) {
Ok(contents) => {
if let Err(e) = fs::write(path, &contents) {
warn!("[save_all_states] Failed to write state file {path:?}: {e}");
}
},
Err(e) => {
warn!("[save_all_states] Failed to serialize state: {e}");
},
}
}
#[cfg(test)]
#[allow(clippy::panic, reason = "tests should panic on unexpected values")]
mod tests {
use std::fs;
use tempfile::NamedTempFile;
use super::load_all_states;
use super::save_all_states;
use crate::state_format::WindowKey;
use crate::types::SavedWindowMode;
use crate::types::WindowState;
fn sample_state() -> WindowState {
WindowState {
logical_position: Some((10, 20)),
logical_width: 800,
logical_height: 600,
monitor_scale: 1.0,
monitor_index: 0,
mode: SavedWindowMode::Windowed,
app_name: "test-app".to_string(),
}
}
#[test]
fn save_then_load_roundtrip_v2() {
let file = match NamedTempFile::new() {
Ok(file) => file,
Err(error) => panic!("failed to create temp file: {error}"),
};
let path = file.path();
let states = std::collections::HashMap::from([
(WindowKey::Primary, sample_state()),
(WindowKey::Managed("primary".to_string()), sample_state()),
]);
save_all_states(path, &states);
let loaded = load_all_states(path);
assert!(loaded.is_some(), "expected saved v1 state to load");
let loaded = loaded.unwrap_or_default();
assert!(loaded.contains_key(&WindowKey::Primary));
assert!(loaded.contains_key(&WindowKey::Managed("primary".to_string())));
}
#[test]
fn legacy_single_window_read_then_save_rewrites_as_v2() {
let file = match NamedTempFile::new() {
Ok(file) => file,
Err(error) => panic!("failed to create temp file: {error}"),
};
let path = file.path();
let legacy_contents = "\
(
position: Some((10, 20)),
width: 800,
height: 600,
monitor_index: 0,
mode: Windowed,
app_name: \"test-app\",
)";
if let Err(error) = fs::write(path, legacy_contents) {
panic!("failed to write legacy content: {error}");
}
let states = load_all_states(path);
assert!(states.is_some(), "expected legacy content to decode");
let states = states.unwrap_or_default();
save_all_states(path, &states);
let contents = fs::read_to_string(path);
assert!(contents.is_ok(), "expected rewritten file to be readable");
let contents = contents.unwrap_or_default();
assert!(
contents.contains("version: 2"),
"expected rewritten file to contain v2 version marker"
);
assert!(
contents.contains("logical_width: 800"),
"expected rewritten file to contain logical_width"
);
}
}