223 lines
6.5 KiB
Rust
223 lines
6.5 KiB
Rust
//! 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<Hsla>,
|
|
pub(crate) palette: PartialPalette,
|
|
}
|
|
|
|
impl OmarchyTheme {
|
|
pub(crate) fn parse(text: &str) -> Option<Self> {
|
|
let table = text.parse::<Table>().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<ThemeVariant> {
|
|
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<Hsla> {
|
|
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<PathBuf> {
|
|
std::env::var_os("HOME").map(PathBuf::from)
|
|
}
|
|
|
|
/// The directory theme-set swaps on every theme change.
|
|
pub(crate) fn current_dir() -> Option<PathBuf> {
|
|
Some(home_dir()?.join(".local/state/omarchy/current"))
|
|
}
|
|
|
|
pub(crate) fn read_current() -> Option<OmarchyTheme> {
|
|
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());
|
|
}
|
|
}
|