x11-overlay 0.1.0

A library for creating overlay interfaces on X11 systems using Cairo for rendering
Documentation
use anyhow::{Context, Result};
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum FontWeight {
    Light,
    Normal,
    Medium,
    Bold,
    Heavy,
}

impl FontWeight {
    pub fn to_cairo_weight(self) -> cairo::FontWeight {
        match self {
            FontWeight::Light => cairo::FontWeight::Normal,
            FontWeight::Normal => cairo::FontWeight::Normal,
            FontWeight::Medium => cairo::FontWeight::Normal,
            FontWeight::Bold => cairo::FontWeight::Bold,
            FontWeight::Heavy => cairo::FontWeight::Bold,
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum FontSlant {
    Normal,
    Italic,
    Oblique,
}

impl FontSlant {
    pub fn to_cairo_slant(self) -> cairo::FontSlant {
        match self {
            FontSlant::Normal => cairo::FontSlant::Normal,
            FontSlant::Italic => cairo::FontSlant::Italic,
            FontSlant::Oblique => cairo::FontSlant::Oblique,
        }
    }
}

#[derive(Debug, Clone)]
pub struct FontDesc {
    pub family: String,
    pub weight: FontWeight,
    pub slant: FontSlant,
    pub size: f64,
}

impl FontDesc {
    pub fn new(family: &str, size: f64) -> Self {
        Self {
            family: family.to_string(),
            weight: FontWeight::Normal,
            slant: FontSlant::Normal,
            size,
        }
    }

    pub fn with_weight(mut self, weight: FontWeight) -> Self {
        self.weight = weight;
        self
    }

    pub fn with_slant(mut self, slant: FontSlant) -> Self {
        self.slant = slant;
        self
    }
}

pub struct FontManager {
    font_dirs: Vec<PathBuf>,
    font_cache: HashMap<String, Vec<PathBuf>>,
    default_fonts: Vec<String>,
}

impl FontManager {
    pub fn new() -> Result<Self> {
        let mut manager = Self {
            font_dirs: Vec::new(),
            font_cache: HashMap::new(),
            default_fonts: vec![
                "DejaVu Sans".to_string(),
                "Liberation Sans".to_string(),
                "sans-serif".to_string(),
                "Arial".to_string(),
            ],
        };

        manager.add_system_font_dirs()?;
        manager.scan_fonts()?;

        Ok(manager)
    }

    fn add_system_font_dirs(&mut self) -> Result<()> {
        let system_dirs = [
            "/usr/share/fonts",
            "/usr/local/share/fonts",
            "/opt/local/share/fonts",
            "~/.fonts",
            "~/.local/share/fonts",
        ];

        for dir in &system_dirs {
            let path = if dir.starts_with('~') {
                if let Some(home) = std::env::var_os("HOME") {
                    PathBuf::from(home).join(&dir[2..])
                } else {
                    continue;
                }
            } else {
                PathBuf::from(dir)
            };

            if path.exists() && path.is_dir() {
                self.font_dirs.push(path);
            }
        }

        Ok(())
    }

    fn scan_fonts(&mut self) -> Result<()> {
        let font_dirs = self.font_dirs.clone();
        for font_dir in &font_dirs {
            self.scan_directory(font_dir)?;
        }
        Ok(())
    }

    fn scan_directory(&mut self, dir: &Path) -> Result<()> {
        if !dir.exists() || !dir.is_dir() {
            return Ok(());
        }

        let entries = fs::read_dir(dir)
            .with_context(|| format!("Failed to read font directory: {}", dir.display()))?;

        for entry in entries {
            let entry = entry?;
            let path = entry.path();

            if path.is_dir() {
                self.scan_directory(&path)?;
            } else if self.is_font_file(&path) {
                if let Some(family) = self.extract_font_family(&path) {
                    self.font_cache
                        .entry(family.to_lowercase())
                        .or_default()
                        .push(path);
                }
            }
        }

        Ok(())
    }

    fn is_font_file(&self, path: &Path) -> bool {
        if let Some(ext) = path.extension() {
            let ext = ext.to_string_lossy().to_lowercase();
            matches!(
                ext.as_str(),
                "ttf" | "otf" | "woff" | "woff2" | "pfb" | "pfa"
            )
        } else {
            false
        }
    }

    fn extract_font_family(&self, _path: &Path) -> Option<String> {
        if let Some(stem) = _path.file_stem() {
            let name = stem.to_string_lossy();

            let family = name.split('-').next().unwrap_or(&name).replace('_', " ");

            Some(family)
        } else {
            None
        }
    }

    pub fn find_font(&self, family: &str) -> Option<&PathBuf> {
        let family_lower = family.to_lowercase();

        if let Some(fonts) = self.font_cache.get(&family_lower) {
            return fonts.first();
        }

        for default_family in &self.default_fonts {
            let default_lower = default_family.to_lowercase();
            if let Some(fonts) = self.font_cache.get(&default_lower) {
                return fonts.first();
            }
        }

        None
    }

    pub fn get_available_families(&self) -> Vec<&String> {
        self.font_cache.keys().collect()
    }

    pub fn has_font(&self, family: &str) -> bool {
        self.font_cache.contains_key(&family.to_lowercase())
    }
}

impl Default for FontManager {
    fn default() -> Self {
        Self::new().unwrap_or_else(|_| Self {
            font_dirs: Vec::new(),
            font_cache: HashMap::new(),
            default_fonts: vec!["sans-serif".to_string()],
        })
    }
}