use crate::persistence::{PersistenceError, WindowState};
use directories::ProjectDirs;
use std::fs;
use std::path::PathBuf;
pub struct WindowSettingsBuilder {
app_name: String,
default_width: u32,
default_height: u32,
default_maximized: bool,
min_size: Option<(u32, u32)>,
max_size: Option<(u32, u32)>,
resizable: bool,
}
impl WindowSettingsBuilder {
pub fn new(app_name: &str) -> Self {
Self {
app_name: app_name.to_string(),
default_width: 800,
default_height: 600,
default_maximized: false,
min_size: None,
max_size: None,
resizable: true,
}
}
pub fn default_size(mut self, width: u32, height: u32) -> Self {
self.default_width = width;
self.default_height = height;
self
}
pub fn default_maximized(mut self, maximized: bool) -> Self {
self.default_maximized = maximized;
self
}
pub fn min_size(mut self, width: u32, height: u32) -> Self {
self.min_size = Some((width, height));
self
}
pub fn max_size(mut self, width: u32, height: u32) -> Self {
self.max_size = Some((width, height));
self
}
pub fn resizable(mut self, resizable: bool) -> Self {
self.resizable = resizable;
self
}
pub fn build(self) -> iced::window::Settings {
let mut state = load_or_default(&self.app_name, self.default_width, self.default_height);
let is_fresh = state.width == self.default_width
&& state.height == self.default_height
&& state.x.is_none()
&& state.y.is_none()
&& !state.maximized;
if is_fresh && self.default_maximized {
state.maximized = true;
}
iced::window::Settings {
size: state.size(),
position: state
.position()
.map(iced::window::Position::Specific)
.unwrap_or(iced::window::Position::Centered),
min_size: self
.min_size
.map(|(w, h)| iced::Size::new(w as f32, h as f32)),
max_size: self
.max_size
.map(|(w, h)| iced::Size::new(w as f32, h as f32)),
resizable: self.resizable,
..Default::default()
}
}
}
impl From<WindowSettingsBuilder> for iced::window::Settings {
fn from(builder: WindowSettingsBuilder) -> Self {
builder.build()
}
}
pub fn get_config_path(app_name: &str) -> Option<PathBuf> {
ProjectDirs::from("", "", app_name).map(|dirs| dirs.config_dir().join("window.json"))
}
fn load_window_state(app_name: &str) -> Result<WindowState, PersistenceError> {
let path = get_config_path(app_name).ok_or_else(|| PersistenceError::NoConfigDir {
app_name: app_name.to_string(),
})?;
if !path.exists() {
return Err(PersistenceError::ReadFailed {
path: path.clone(),
source: std::io::Error::new(std::io::ErrorKind::NotFound, "File not found"),
});
}
let content = fs::read_to_string(&path).map_err(|e| PersistenceError::ReadFailed {
path: path.clone(),
source: e,
})?;
let state: WindowState =
serde_json::from_str(&content).map_err(|e| PersistenceError::ParseFailed {
path: path.clone(),
source: e,
})?;
state.validate()?;
Ok(state)
}
pub fn load_or_default(app_name: &str, default_width: u32, default_height: u32) -> WindowState {
#[cfg(debug_assertions)]
println!("DEBUG: Loading window state for '{}'", app_name);
match load_window_state(app_name) {
Ok(mut state) => {
#[cfg(debug_assertions)]
println!("DEBUG: Loaded state: {:?}", state);
#[allow(clippy::collapsible_if)]
if let (Some(x), Some(y)) = (state.x, state.y) {
if !crate::persistence::monitor::position_is_reasonable(x, y) {
tracing::warn!("Ignoring unreasonable window position: {}, {}", x, y);
state.x = None;
state.y = None;
}
}
state
}
Err(e) => {
#[cfg(debug_assertions)]
println!("DEBUG: Failed to load state: {}", e);
let is_not_found = matches!(
&e,
PersistenceError::ReadFailed { source, .. }
if source.kind() == std::io::ErrorKind::NotFound
);
if !is_not_found {
tracing::warn!("Failed to load window state for '{}': {}", app_name, e);
}
WindowState::with_defaults(default_width, default_height)
}
}
}
pub fn save_window_state(app_name: &str, state: &WindowState) -> Result<(), PersistenceError> {
#[cfg(debug_assertions)]
println!("DEBUG: Saving window state for '{}': {:?}", app_name, state);
let path = get_config_path(app_name).ok_or_else(|| {
let e = PersistenceError::NoConfigDir {
app_name: app_name.to_string(),
};
tracing::warn!("Failed to save window state: {}", e);
e
})?;
let parent = path.parent().ok_or_else(|| {
let e = PersistenceError::WriteFailed {
path: path.clone(),
source: std::io::Error::other("Invalid path"),
};
tracing::warn!("Failed to save window state: {}", e);
e
})?;
#[allow(clippy::collapsible_if)]
if !parent.exists() {
if let Err(e) = fs::create_dir_all(parent) {
let err = PersistenceError::CreateDirFailed {
path: parent.to_path_buf(),
source: e,
};
tracing::warn!("Failed to save window state: {}", err);
return Err(err);
}
}
let temp_path = path.with_extension("tmp");
let json = match serde_json::to_string_pretty(state) {
Ok(j) => j,
Err(e) => {
let err = PersistenceError::WriteFailed {
path: path.clone(),
source: std::io::Error::other(e),
};
tracing::warn!("Failed to save window state: {}", err);
return Err(err);
}
};
if let Err(e) = fs::write(&temp_path, json) {
let err = PersistenceError::WriteFailed {
path: temp_path.clone(),
source: e,
};
tracing::warn!("Failed to save window state: {}", err);
return Err(err);
}
if let Err(e) = fs::rename(&temp_path, &path) {
let err = PersistenceError::WriteFailed {
path: path.clone(),
source: e,
};
tracing::warn!("Failed to save window state: {}", err);
return Err(err);
}
Ok(())
}