use std::fmt::Write as _;
use std::path::{Path, PathBuf};
use std::process::Command;
use serde::Deserialize;
use crate::config::{
DEFAULT_STYLESHEET, LoadProblem, StyleSource, Stylesheet, SystemTheme, ThemeConfig, ThemeMode,
default_structure,
};
use crate::highlight;
#[derive(Debug, Clone, PartialEq)]
pub struct OmarchyTheme {
pub name: String,
pub colors: Colors,
pub font_family: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct Colors {
pub accent: String,
pub selection: String,
pub muted: String,
pub background: String,
pub dark_background: String,
pub darker_background: String,
pub lighter_background: String,
pub foreground: String,
pub dark_foreground: String,
pub bright_foreground: String,
pub red: String,
pub green: String,
#[serde(default)]
pub blue: String,
#[serde(default)]
pub cyan: String,
#[serde(default)]
pub yellow: String,
#[serde(default)]
pub orange: String,
#[serde(default)]
pub magenta: String,
}
impl Colors {
fn palette(&self) -> [(&'static str, &str); 13] {
[
("tn_bg", &self.background),
("tn_bg_dark", &self.dark_background),
("tn_bg_darker", &self.darker_background),
("tn_bg_light", &self.lighter_background),
("tn_fg", &self.foreground),
("tn_fg_bright", &self.bright_foreground),
("tn_fg_dim", &self.dark_foreground),
("tn_muted", &self.muted),
("tn_selection", &self.selection),
("tn_blue", &self.accent),
("tn_accent", &self.accent),
("tn_green", &self.green),
("tn_red", &self.red),
]
}
}
pub fn omarchy_stylesheet() -> Stylesheet {
match load_omarchy_theme() {
Some(theme) => Stylesheet {
css: render(&theme),
source: StyleSource::System,
problem: None,
},
None => Stylesheet {
css: DEFAULT_STYLESHEET.to_owned(),
source: StyleSource::Builtin,
problem: Some(LoadProblem::SystemTheme(
"no active Omarchy theme found; using the built-in default".to_owned(),
)),
},
}
}
pub fn syntax_palette(theme: &ThemeConfig) -> highlight::Palette {
match theme.mode {
ThemeMode::Custom => highlight::Palette::dark(),
ThemeMode::System => match theme.system {
SystemTheme::Omarchy => load_omarchy_theme()
.map(|theme| palette_from(&theme.colors))
.unwrap_or_else(highlight::Palette::dark),
},
}
}
fn palette_from(colors: &Colors) -> highlight::Palette {
let fallback = highlight::Palette::dark();
let pick = |value: &str, default: &str| {
if value.is_empty() { default.to_owned() } else { value.to_owned() }
};
highlight::Palette {
keyword: pick(&colors.accent, &fallback.keyword),
type_: pick(&colors.cyan, &fallback.type_),
function: pick(&colors.blue, &fallback.function),
string: pick(&colors.green, &fallback.string),
comment: pick(&colors.dark_foreground, &fallback.comment),
number: pick(&colors.orange, &fallback.number),
constant: pick(&colors.magenta, &fallback.constant),
preprocessor: pick(&colors.yellow, &fallback.preprocessor),
tag: pick(&colors.red, &fallback.tag),
attribute: pick(&colors.magenta, &fallback.attribute),
heading: pick(&colors.accent, &fallback.heading),
link: pick(&colors.cyan, &fallback.link),
}
}
pub fn load_omarchy_theme() -> Option<OmarchyTheme> {
let dir = current_theme_dir()?;
load_omarchy_theme_from(&dir)
}
pub fn load_omarchy_theme_from(dir: &Path) -> Option<OmarchyTheme> {
let raw = std::fs::read_to_string(dir.join("colors.toml")).ok()?;
let colors: Colors = toml::from_str(&raw).ok()?;
let name = std::fs::read_to_string(dir.join("theme.name"))
.map(|name| name.trim().to_owned())
.unwrap_or_default();
Some(OmarchyTheme { name, colors, font_family: omarchy_font_family() })
}
pub fn render(theme: &OmarchyTheme) -> String {
let mut css = String::from(
"/* Generated from the active Omarchy theme; main.css is not read in system mode. */\n",
);
for (name, value) in theme.colors.palette() {
let _ = writeln!(css, "@define-color {name} {value};");
}
css.push_str(default_structure());
if let Some(family) = &theme.font_family {
let _ = write!(
css,
"\n.panel, .bookmark-editor-root, .props {{ font-family: \"{family}\"; }}\n"
);
}
css
}
fn current_theme_dir() -> Option<PathBuf> {
let state = std::env::var_os("XDG_STATE_HOME")
.filter(|value| !value.is_empty())
.map(PathBuf::from)
.or_else(|| {
std::env::var_os("HOME")
.filter(|home| !home.is_empty())
.map(|home| PathBuf::from(home).join(".local/state"))
})?;
Some(state.join("omarchy").join("current").join("theme"))
}
fn omarchy_font_family() -> Option<String> {
let output = Command::new("fc-match")
.args(["monospace", "-f", "%{family}"])
.output()
.ok()?;
if !output.status.success() {
return None;
}
let families = String::from_utf8(output.stdout).ok()?;
let first = families.split(',').next()?.trim();
(!first.is_empty()).then(|| first.to_owned())
}
#[cfg(test)]
mod tests {
use super::*;
const SAMPLE: &str = r##"
mode = "dark"
accent = "#7aa2f7"
selection = "#292e42"
muted = "#414868"
background = "#1a1b26"
dark_background = "#13141c"
darker_background = "#0e0e14"
lighter_background = "#24283b"
foreground = "#a9b1d6"
dark_foreground = "#565f89"
bright_foreground = "#c0caf5"
red = "#f7768e"
green = "#9ece6a"
blue = "#7aa2f7"
"##;
fn sample_colors() -> Colors {
toml::from_str(SAMPLE).unwrap()
}
#[test]
fn colors_parse_from_colors_toml() {
let colors = sample_colors();
assert_eq!(colors.background, "#1a1b26");
assert_eq!(colors.accent, "#7aa2f7");
assert_eq!(colors.green, "#9ece6a");
}
#[test]
fn render_defines_the_palette_and_reuses_the_structure() {
let theme = OmarchyTheme {
name: "tokyo-night".to_owned(),
colors: sample_colors(),
font_family: Some("JetBrainsMono Nerd Font".to_owned()),
};
let css = render(&theme);
assert!(css.contains("@define-color tn_bg #1a1b26;"));
assert!(css.contains("@define-color tn_accent #7aa2f7;"));
assert!(css.contains("@define-color tn_green #9ece6a;"));
assert!(css.contains("@define-color tn_red #f7768e;"));
assert!(css.contains("@define-color tn_blue #7aa2f7;"));
assert!(css.contains(".tree-row {"));
let structure = css.split_once(".tree-row {").unwrap().1;
assert!(!structure.contains("@define-color tn_bg"));
assert!(css.contains(
".panel, .bookmark-editor-root, .props { font-family: \"JetBrainsMono Nerd Font\"; }"
));
}
#[test]
fn render_omits_the_font_override_without_a_family() {
let without = render(&OmarchyTheme {
name: String::new(),
colors: sample_colors(),
font_family: None,
});
assert!(!without.contains(".panel, .bookmark-editor-root, .props {"));
}
#[test]
fn missing_colors_toml_is_not_a_theme() {
let dir = tempfile::tempdir().unwrap();
assert!(load_omarchy_theme_from(dir.path()).is_none());
}
#[test]
fn theme_loads_from_an_explicit_directory() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("colors.toml"), SAMPLE).unwrap();
std::fs::write(dir.path().join("theme.name"), "tokyo-night\n").unwrap();
let theme = load_omarchy_theme_from(dir.path()).unwrap();
assert_eq!(theme.name, "tokyo-night");
assert_eq!(theme.colors.background, "#1a1b26");
}
}