sequoia-platform 0.1.0

Sequoia GUI framework — windowing platform (winit + wgpu)
use std::fs::File;
use std::path::PathBuf;

pub struct SingleInstance {
    path: PathBuf,
}

impl SingleInstance {
    fn lock_path(app_id: &str) -> PathBuf {
        let safe: String = app_id
            .chars()
            .map(|c| {
                if c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.' {
                    c
                } else {
                    '_'
                }
            })
            .collect();
        std::env::temp_dir().join(format!("sequoia-single-{safe}.lock"))
    }

    pub fn acquire(app_id: &str) -> Result<Self, ()> {
        let path = Self::lock_path(app_id);
        match File::create_new(&path) {
            Ok(_) => Ok(Self { path }),
            Err(_) => Err(()),
        }
    }

    pub fn path(&self) -> &PathBuf {
        &self.path
    }

    pub fn lock_path_for(app_id: &str) -> PathBuf {
        Self::lock_path(app_id)
    }
}

impl Drop for SingleInstance {
    fn drop(&mut self) {
        let _ = std::fs::remove_file(&self.path);
    }
}

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

    #[test]
    fn second_acquire_fails_until_drop() {
        let id = format!("test-{}", std::process::id());
        let g = SingleInstance::acquire(&id).expect("pierwsza");
        assert!(SingleInstance::acquire(&id).is_err(), "druga odpada");
        drop(g);
        assert!(
            SingleInstance::acquire(&id).is_ok(),
            "po drop blokada wolna"
        );
    }

    #[test]
    fn lock_path_is_sanitized() {
        let p = SingleInstance::lock_path_for("a/b:c d");
        let name = p.file_name().unwrap().to_string_lossy();
        assert!(!name.contains('/') && !name.contains(':') && !name.contains(' '));
    }
}