theme: add linux system theme sources
This commit is contained in:
@@ -0,0 +1,221 @@
|
||||
//! 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,
|
||||
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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user