float-clock-wayland 0.2.0

Always-on-top floating desktop clock widget for Wayland compositors.
use serde::{Deserialize, Serialize};
use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct WindowState {
    pub x: i32,
    pub y: i32,
}

impl Default for WindowState {
    fn default() -> Self {
        Self { x: 50, y: 50 }
    }
}

/// Retrieves the path to the state directory (conforming to XDG State Home specification).
pub fn get_state_dir() -> PathBuf {
    std::env::var("XDG_STATE_HOME").map_or_else(|_| {
            let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
            PathBuf::from(home).join(".local").join("state")
        }, PathBuf::from)
        .join("float-clock-wayland")
}

/// POSIX atomic file writing utility. Writes to a temporary file first, then renames to target.
pub fn write_atomic(path: &Path, content: &str) -> std::io::Result<()> {
    let parent = path.parent().unwrap_or_else(|| Path::new("."));
    fs::create_dir_all(parent)?;

    let pid = std::process::id();
    let filename = path.file_name().unwrap_or_default().to_string_lossy();
    let tmp_path = parent.join(format!(".tmp_{pid}_{filename}"));

    let mut file = fs::File::create(&tmp_path)?;
    file.write_all(content.as_bytes())?;
    file.sync_all()?;
    drop(file);

    fs::rename(&tmp_path, path)
}

pub fn load_window_state() -> WindowState {
    let path = get_state_dir().join("state.json");
    if let Ok(contents) = fs::read_to_string(&path)
        && let Ok(state) = serde_json::from_str::<WindowState>(&contents) {
            return state;
        }
    WindowState::default()
}

pub fn save_window_state(state: WindowState) {
    let dir = get_state_dir();
    if let Err(e) = fs::create_dir_all(&dir) {
        log::warn!("Could not create state directory {dir:?}: {e}");
        return;
    }
    let path = dir.join("state.json");
    if let Ok(json_str) = serde_json::to_string_pretty(&state)
        && let Err(e) = write_atomic(&path, &json_str) {
            log::warn!("Could not save window state to {path:?}: {e}");
        }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_window_state_serialization() {
        let state = WindowState { x: 140, y: 280 };
        let json = serde_json::to_string(&state).unwrap();
        let deserialized: WindowState = serde_json::from_str(&json).unwrap();
        assert_eq!(state, deserialized);
    }
}