xnano 0.1.1

Text editor (inspired by nano) with extra features such as syntax highlighting, themes, etc. Written entirely in Rust.
use crate::editor::Editor;
use crossterm::style::Color;
use syntect::highlighting::Theme;
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use include_dir::{include_dir, Dir};

static BUNDLED_THEMES: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/themes");

pub trait ConfigExt {
    fn get_base_dir() -> Option<PathBuf>;
    fn initialize_themes() -> std::io::Result<()>;
    fn get_config_path() -> Option<PathBuf>;
    fn get_theme_dir() -> Option<PathBuf>;
    fn load_config() -> (String, bool, bool);
    fn save_config(&self);
    fn is_dark_theme(theme: &Theme) -> bool;
    fn derive_ui_color(bg: syntect::highlighting::Color, is_dark: bool) -> Color;
    fn cycle_theme(&mut self);
    fn update_cursor_color(&self);
}

impl ConfigExt for Editor {
    fn get_base_dir() -> Option<PathBuf> {
        let home = env::var("HOME").or_else(|_| env::var("USERPROFILE")).unwrap_or_default();
        if home.is_empty() {
            None
        } else {
            let path = Path::new(&home).join(".xnano");
            let _ = fs::create_dir_all(&path);
            Some(path)
        }
    }

    fn initialize_themes() -> std::io::Result<()> {
        if let Some(theme_dir) = Self::get_theme_dir() {
            if fs::read_dir(&theme_dir)?.next().is_none() {
                for file in BUNDLED_THEMES.files() {
                    let path = theme_dir.join(file.path());
                    fs::write(path, file.contents())?;
                }
            }
        }
        Ok(())
    }

    fn get_config_path() -> Option<PathBuf> {
        Self::get_base_dir().map(|p| p.join("xnanorc"))
    }

    fn get_theme_dir() -> Option<PathBuf> {
        Self::get_base_dir().map(|p| {
            let theme_path = p.join("themes");
            let _ = fs::create_dir_all(&theme_path);
            theme_path
        })
    }

    fn load_config() -> (String, bool, bool) {
        let mut theme = String::from("base16-ocean.dark");
        let mut line_numbers = false;
        let mut soft_wrap = false;

        if let Some(path) = Self::get_config_path() {
            if let Ok(content) = fs::read_to_string(path) {
                for line in content.lines() {
                    let parts: Vec<&str> = line.splitn(2, '=').collect();
                    if parts.len() == 2 {
                        match parts[0] {
                            "theme" => theme = parts[1].to_string(),
                            "line_numbers" => line_numbers = parts[1] == "true",
                            "soft_wrap" => soft_wrap = parts[1] == "true",
                            _ => {}
                        }
                    }
                }
            }
        }
        (theme, line_numbers, soft_wrap)
    }

    fn save_config(&self) {
        if let Some(path) = Self::get_config_path() {
            let content = format!(
                "theme={}\nline_numbers={}\nsoft_wrap={}\n",
                self.current_theme, self.show_line_numbers, self.soft_wrap
            );
            let _ = fs::write(path, content);
        }
    }

    fn is_dark_theme(theme: &Theme) -> bool {
        let bg = theme.settings.background.unwrap_or(syntect::highlighting::Color { r: 0, g: 0, b: 0, a: 255 });
        let luminance = 0.299 * (bg.r as f32) + 0.587 * (bg.g as f32) + 0.114 * (bg.b as f32);
        luminance < 128.0
    }

    fn derive_ui_color(bg: syntect::highlighting::Color, is_dark: bool) -> Color {
        let offset: i16 = if is_dark { 20 } else { -20 };
        let r = (bg.r as i16 + offset).clamp(0, 255) as u8;
        let g = (bg.g as i16 + offset).clamp(0, 255) as u8;
        let b = (bg.b as i16 + offset).clamp(0, 255) as u8;
        Color::Rgb { r, g, b }
    }

    fn cycle_theme(&mut self) {
        let mut themes: Vec<String> = self.theme_set.themes.keys().cloned().collect();
        themes.sort();

        if let Some(current_idx) = themes.iter().position(|t| t == &self.current_theme) {
            let next_idx = (current_idx + 1) % themes.len();
            self.current_theme = themes[next_idx].clone();

            self.save_config();
            self.clear_cache();

            self.status_message = format!("Theme changed to: {}", self.current_theme);
            self.status_time = Some(std::time::Instant::now());

            // Trigger the cursor color update whenever the theme changes
            self.update_cursor_color();
        }
    }

    fn update_cursor_color(&self) {
        print!("\x1b]12;#888888\x07");
        let _ = std::io::Write::flush(&mut std::io::stdout());
    }
}