//! Omarchy adapter: reads the current theme's `colors.toml` from the state //! directory (`~/.local/state/omarchy/current/theme/`). The parse is pure, //! so fixtures run on every OS; the IO lives behind the Linux cfg. use gpui::Hsla; use toml::Table; use crate::system::{PartialPalette, SourceKind, SystemTheme}; use crate::{AccessibilityPreferences, ThemeVariant}; /// The palette Omarchy deploys with the current theme. #[derive(Clone, Copy, Debug, PartialEq)] pub(crate) struct OmarchyTheme { pub(crate) variant: ThemeVariant, pub(crate) accent: Option, pub(crate) palette: PartialPalette, } impl OmarchyTheme { pub(crate) fn parse(text: &str) -> Option { let table = text.parse::().ok()?; let background = hex_color(&table, "background")?; let foreground = hex_color(&table, "foreground")?; let variant = table .get("mode") .and_then(toml::Value::as_str) .and_then(mode_variant) .unwrap_or_else(|| variant_by_lightness(background)); let accent = hex_color(&table, "accent").or_else(|| hex_color(&table, "color4")); let selection = hex_color(&table, "selection"); let muted_foreground = hex_color(&table, "dark_foreground").or_else(|| hex_color(&table, "color8")); let palette = PartialPalette { background: Some(background), foreground: Some(foreground), surface: hex_color(&table, "lighter_background"), surface_foreground: Some(foreground), muted_foreground, destructive: hex_color(&table, "red").or_else(|| hex_color(&table, "color1")), success: hex_color(&table, "green").or_else(|| hex_color(&table, "color2")), warning: hex_color(&table, "yellow").or_else(|| hex_color(&table, "orange")), ring: accent, selection, ..PartialPalette::default() }; Some(Self { variant, accent, palette, }) } pub(crate) fn into_system(self, accessibility: AccessibilityPreferences) -> SystemTheme { SystemTheme { variant: self.variant, accent: self.accent, palette: self.palette, control_radius: None, input_height: None, titlebar_height: None, root_font_size: None, accessibility, source: SourceKind::Omarchy, } } } fn mode_variant(mode: &str) -> Option { match mode { "dark" => Some(ThemeVariant::Dark), "light" => Some(ThemeVariant::Light), _ => None, } } fn variant_by_lightness(background: Hsla) -> ThemeVariant { if background.l >= 0.5 { ThemeVariant::Light } else { ThemeVariant::Dark } } fn hex_color(table: &Table, key: &str) -> Option { let value = table.get(key)?.as_str()?; let digits = value.trim().strip_prefix('#')?; if digits.len() != 6 { return None; } let hex = u32::from_str_radix(digits, 16).ok()?; Some(Hsla::from(gpui::rgb(hex))) } #[cfg(target_os = "linux")] pub(crate) mod io { use std::path::PathBuf; use super::OmarchyTheme; fn home_dir() -> Option { std::env::var_os("HOME").map(PathBuf::from) } /// The directory theme-set swaps on every theme change. pub(crate) fn current_dir() -> Option { Some(home_dir()?.join(".local/state/omarchy/current")) } pub(crate) fn read_current() -> Option { let text = std::fs::read_to_string( home_dir()?.join(".local/state/omarchy/current/theme/colors.toml"), ) .ok()?; OmarchyTheme::parse(&text) } } #[cfg(test)] mod tests { use super::*; const TOKYO_NIGHT: &str = "\ mode = \"dark\" accent = \"#7aa2f7\" selection = \"#292e42\" muted = \"#414868\" background = \"#1a1b26\" dark_background = \"#13141c\" darker_background = \"#0e0e14\" lighter_background = \"#24283b\" foreground = \"#a9b1d6\" dark_foreground = \"#565f89\" red = \"#f7768e\" yellow = \"#e0af68\" orange = \"#eb927b\" green = \"#9ece6a\" "; const ANSI_GENERATED: &str = "\ accent = \"#0060ff\" selection = \"#c1c8d2\" background = \"#e8e8ec\" foreground = \"#251f32\" color0 = \"#e8e8ec\" color1 = \"#ab2833\" color2 = \"#3f7c38\" color3 = \"#8f6235\" color4 = \"#0060ff\" color5 = \"#8d439d\" color6 = \"#3e7d96\" color7 = \"#251f32\" color8 = \"#625e6a\" color9 = \"#cd3d45\" color10 = \"#44a540\" color11 = \"#c18a41\" color12 = \"#278aff\" color13 = \"#a659b9\" color14 = \"#4b9fc0\" color15 = \"#635d6f\" "; #[test] fn semantic_keys_map_onto_roles() { let theme = OmarchyTheme::parse(TOKYO_NIGHT).expect("tokyo-night parses"); assert_eq!(theme.variant, ThemeVariant::Dark); assert_eq!(theme.accent, Some(Hsla::from(gpui::rgb(0x7aa2f7)))); let palette = theme.palette; let background = palette.background.expect("background stated"); assert_eq!(background, Hsla::from(gpui::rgb(0x1a1b26))); assert_eq!(palette.surface, Some(Hsla::from(gpui::rgb(0x24283b)))); assert_eq!( palette.muted_foreground, Some(Hsla::from(gpui::rgb(0x565f89))) ); assert_eq!(palette.destructive, Some(Hsla::from(gpui::rgb(0xf7768e)))); assert_eq!(palette.success, Some(Hsla::from(gpui::rgb(0x9ece6a)))); assert_eq!(palette.warning, Some(Hsla::from(gpui::rgb(0xe0af68)))); assert_eq!(palette.selection, Some(Hsla::from(gpui::rgb(0x292e42)))); assert_eq!(palette.ring, theme.accent); } #[test] fn ansi_fallback_derives_the_variant_from_the_background() { let theme = OmarchyTheme::parse(ANSI_GENERATED).expect("ANSI palette parses"); assert_eq!(theme.variant, ThemeVariant::Light); assert_eq!(theme.accent, Some(Hsla::from(gpui::rgb(0x0060ff)))); assert_eq!( theme.palette.destructive, Some(Hsla::from(gpui::rgb(0xab2833))) ); assert_eq!( theme.palette.muted_foreground, Some(Hsla::from(gpui::rgb(0x625e6a))) ); } #[test] fn a_stated_mode_beats_the_derivation() { let text = "mode = \"light\"\naccent = \"#7aa2f7\"\nbackground = \"#1a1b26\"\nforeground = \"#a9b1d6\"\n"; let theme = OmarchyTheme::parse(text).expect("parses"); assert_eq!(theme.variant, ThemeVariant::Light); } #[test] fn non_toml_text_is_rejected() { assert!(OmarchyTheme::parse("not a theme").is_none()); } }