use std::path::PathBuf;
use ratatui::style::{Color, Style};
use serde::Deserialize;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum BackgroundMode {
#[default]
Opaque,
Transparent,
}
impl BackgroundMode {
pub const SETTING_KEY: &str = "ui_background";
#[must_use]
pub const fn key(self) -> &'static str {
match self {
Self::Opaque => "opaque",
Self::Transparent => "transparent",
}
}
#[must_use]
pub const fn label(self) -> &'static str {
self.key()
}
#[must_use]
pub const fn next(self) -> Self {
match self {
Self::Opaque => Self::Transparent,
Self::Transparent => Self::Opaque,
}
}
#[must_use]
pub fn parse(value: &str) -> Option<Self> {
let value = value.trim();
if value.eq_ignore_ascii_case("opaque")
|| value.eq_ignore_ascii_case("solid")
|| value.eq_ignore_ascii_case("on")
{
Some(Self::Opaque)
} else if value.eq_ignore_ascii_case("transparent")
|| value.eq_ignore_ascii_case("terminal")
|| value.eq_ignore_ascii_case("reset")
|| value.eq_ignore_ascii_case("off")
{
Some(Self::Transparent)
} else {
None
}
}
}
#[derive(Clone, Copy)]
pub struct Theme {
pub bg: Color,
pub surface: Color,
pub fg: Color,
pub fg_dim: Color,
pub accent: Color,
pub accent2: Color,
pub border: Color,
pub border_dim: Color,
pub success: Color,
pub warning: Color,
pub error: Color,
pub user_msg: Color,
pub assistant_msg: Color,
pub tool_msg: Color,
pub research_msg: Color,
}
impl Default for Theme {
fn default() -> Self {
Self {
bg: Color::Black,
surface: Color::Black,
fg: Color::White,
fg_dim: Color::DarkGray,
accent: Color::Cyan,
accent2: Color::Magenta,
border: Color::Cyan,
border_dim: Color::DarkGray,
success: Color::Green,
warning: Color::Yellow,
error: Color::Red,
user_msg: Color::Cyan,
assistant_msg: Color::White,
tool_msg: Color::Yellow,
research_msg: Color::Magenta,
}
}
}
#[derive(Deserialize)]
struct Alacritty {
colors: ColorsSection,
}
#[derive(Deserialize)]
struct ColorsSection {
primary: Primary,
normal: Palette,
bright: Palette,
}
#[derive(Deserialize)]
struct Primary {
background: String,
foreground: String,
}
#[derive(Deserialize)]
struct Palette {
black: String,
red: String,
green: String,
yellow: String,
magenta: String,
cyan: String,
}
fn hex_to_color(s: &str) -> Option<Color> {
let s = s.trim_start_matches('#');
if s.len() != 6 {
return None;
}
let r = u8::from_str_radix(&s[0..2], 16).ok()?;
let g = u8::from_str_radix(&s[2..4], 16).ok()?;
let b = u8::from_str_radix(&s[4..6], 16).ok()?;
Some(Color::Rgb(r, g, b))
}
impl Theme {
fn from_alacritty(a: &Alacritty) -> Self {
let fallback = Self::default();
let n = &a.colors.normal;
let b = &a.colors.bright;
let bg = hex_to_color(&a.colors.primary.background).unwrap_or(fallback.bg);
Self {
bg,
surface: bg,
fg: hex_to_color(&a.colors.primary.foreground).unwrap_or(fallback.fg),
fg_dim: hex_to_color(&b.black).unwrap_or(fallback.fg_dim),
accent: hex_to_color(&n.cyan).unwrap_or(fallback.accent),
accent2: hex_to_color(&n.magenta).unwrap_or(fallback.accent2),
border: hex_to_color(&n.cyan).unwrap_or(fallback.border),
border_dim: hex_to_color(&b.black).unwrap_or(fallback.border_dim),
success: hex_to_color(&n.green).unwrap_or(fallback.success),
warning: hex_to_color(&n.yellow).unwrap_or(fallback.warning),
error: hex_to_color(&n.red).unwrap_or(fallback.error),
user_msg: hex_to_color(&n.cyan).unwrap_or(fallback.user_msg),
assistant_msg: hex_to_color(&a.colors.primary.foreground)
.unwrap_or(fallback.assistant_msg),
tool_msg: hex_to_color(&n.yellow).unwrap_or(fallback.tool_msg),
research_msg: hex_to_color(&n.magenta).unwrap_or(fallback.research_msg),
}
}
pub fn set_background_mode(&mut self, mode: BackgroundMode) {
self.surface = match mode {
BackgroundMode::Opaque => self.bg,
BackgroundMode::Transparent => Color::Reset,
};
}
#[must_use]
pub fn background_style(&self) -> Style {
Style::default().bg(self.surface)
}
}
fn omarchy_current_dir() -> Option<PathBuf> {
Some(std::env::home_dir()?.join(".config/omarchy/current"))
}
pub fn current_link_target() -> Option<PathBuf> {
std::fs::read_link(omarchy_current_dir()?).ok()
}
pub fn load() -> Theme {
omarchy_current_dir()
.map(|d| d.join("theme/alacritty.toml"))
.and_then(|p| std::fs::read_to_string(p).ok())
.and_then(|s| toml::from_str::<Alacritty>(&s).ok())
.map(|a| Theme::from_alacritty(&a))
.unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_omarchy_alacritty_toml() {
let toml = r##"
[colors.primary]
background = "#0B0C16"
foreground = "#ddf7ff"
[colors.normal]
black = "#0B0C16"
red = "#50f872"
green = "#4fe88f"
yellow = "#50f7d4"
blue = "#829dd4"
magenta = "#86a7df"
cyan = "#7cf8f7"
white = "#85E1FB"
[colors.bright]
black = "#6a6e95"
red = "#85ff9d"
green = "#9cf7c2"
yellow = "#a4ffec"
blue = "#c4d2ed"
magenta = "#cddbf4"
cyan = "#d1fffe"
white = "#ddf7ff"
"##;
let a: Alacritty = toml::from_str(toml).unwrap();
let theme = Theme::from_alacritty(&a);
assert_eq!(theme.accent, Color::Rgb(0x7c, 0xf8, 0xf7));
assert_eq!(theme.fg, Color::Rgb(0xdd, 0xf7, 0xff));
}
#[test]
fn background_mode_parses_and_cycles() {
assert_eq!(BackgroundMode::parse("solid"), Some(BackgroundMode::Opaque));
assert_eq!(
BackgroundMode::parse("terminal"),
Some(BackgroundMode::Transparent)
);
assert_eq!(BackgroundMode::Opaque.next(), BackgroundMode::Transparent);
assert_eq!(BackgroundMode::Transparent.next(), BackgroundMode::Opaque);
}
#[test]
fn background_mode_updates_surface_color() {
let mut theme = Theme::default();
theme.set_background_mode(BackgroundMode::Transparent);
assert_eq!(theme.surface, Color::Reset);
theme.set_background_mode(BackgroundMode::Opaque);
assert_eq!(theme.surface, theme.bg);
}
#[test]
fn missing_file_falls_back_to_default() {
let _ = load();
}
#[test]
fn malformed_toml_falls_back_to_default() {
let bad = "not = [valid";
let parsed: Option<Alacritty> = toml::from_str(bad).ok();
assert!(parsed.is_none());
}
}