xnano 0.2.0

Efficient, fast text editor with syntax highlighting, themes, spellcheck. Written from the ground-up in Rust. Tested with macOS, Linux, Windows, BSD.
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");

#[derive(Clone, Copy)]
pub struct UiColors {
    pub bg: Color,
    pub fg: Color,
    pub menu_bg: Color,
    pub selected_bg: Color,
    pub accent: Color,
    pub is_dark: bool,
}

pub trait ConfigExt {
    fn cycle_theme(&mut self);
    fn derive_ui_colors(theme: &Theme) -> UiColors;
    fn get_base_dir() -> Option<PathBuf>;
    fn get_config_path() -> Option<PathBuf>;
    fn get_theme_dir() -> Option<PathBuf>;
    fn initialize_themes() -> std::io::Result<()>;
    fn load_config() -> (String, bool, bool);
    fn save_config(&self);
    fn update_cursor_color(&self);
}

impl ConfigExt for Editor {
    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 derive_ui_colors(theme: &Theme) -> UiColors {
        let raw_bg = theme.settings.background.unwrap_or(syntect::highlighting::Color { r: 0, g: 0, b: 0, a: 255 });
        let raw_fg = theme.settings.foreground.unwrap_or(syntect::highlighting::Color { r: 255, g: 255, b: 255, a: 255 });

        let bg = Color::Rgb { r: raw_bg.r, g: raw_bg.g, b: raw_bg.b };
        let fg = Color::Rgb { r: raw_fg.r, g: raw_fg.g, b: raw_fg.b };

        // determine luminance
        let is_dark = (raw_bg.r as u32 + raw_bg.g as u32 + raw_bg.b as u32) < 384;

        let ui_bg = if is_dark {
            Color::Rgb { r: raw_bg.r.saturating_add(20), g: raw_bg.g.saturating_add(20), b: raw_bg.b.saturating_add(20) }
        } else {
            Color::Rgb { r: raw_bg.r.saturating_sub(20), g: raw_bg.g.saturating_sub(20), b: raw_bg.b.saturating_sub(20) }
        };

        let selected_bg = if raw_bg.r < 128 {
            Color::Rgb { r: raw_bg.r.saturating_add(40), g: raw_bg.g.saturating_add(40), b: raw_bg.b.saturating_add(40) }
        } else {
            Color::Rgb { r: raw_bg.r.saturating_sub(40), g: raw_bg.g.saturating_sub(40), b: raw_bg.b.saturating_sub(40) }
        };

        let get_theme_color = |keys: &[&str]| -> Option<Color> {
            for item in &theme.scopes {
                let scope_str = format!("{:?}", item.scope).to_lowercase();
                for key in keys {
                    if scope_str.contains(key) {
                        if let Some(c) = item.style.foreground {
                            return Some(Color::Rgb { r: c.r, g: c.g, b: c.b });
                        }
                    }
                }
            }
            None
        };

        let mut accent = get_theme_color(&["entity.name.function", "variable"])
            .unwrap_or(if is_dark { Color::Rgb { r: 100, g: 200, b: 255 } } else { Color::Rgb { r: 20, g: 100, b: 180 } });

        if let Some(name) = &theme.name {
            let lower_name = name.to_lowercase();

            if lower_name.contains("catppuccin") {
                accent = if is_dark {
                    Color::Rgb { r: 249, g: 226, b: 175 }
                } else {
                    Color::Rgb { r: 223, g: 142, b: 29 }
                };
            } else if lower_name.contains("base16") {
                accent = if is_dark {
                    Color::Rgb { r: 250, g: 188, b: 45 }
                } else {
                    Color::Rgb { r: 200, g: 60, b: 20 }
                };
            }
        }

        UiColors { bg, fg, menu_bg: ui_bg, selected_bg, accent, is_dark }

    }

    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 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 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 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 update_cursor_color(&self) {
        print!("\x1b]12;#888888\x07");
        let _ = std::io::Write::flush(&mut std::io::stdout());
    }
}