#![forbid(unsafe_code)]
use crate::color::Color;
use std::env;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AdaptiveColor {
Fixed(Color),
Adaptive {
light: Color,
dark: Color,
},
}
impl AdaptiveColor {
#[inline]
pub const fn fixed(color: Color) -> Self {
Self::Fixed(color)
}
#[inline]
pub const fn adaptive(light: Color, dark: Color) -> Self {
Self::Adaptive { light, dark }
}
#[inline]
pub const fn resolve(&self, is_dark: bool) -> Color {
match self {
Self::Fixed(c) => *c,
Self::Adaptive { light, dark } => {
if is_dark {
*dark
} else {
*light
}
}
}
}
#[inline]
pub const fn is_adaptive(&self) -> bool {
matches!(self, Self::Adaptive { .. })
}
}
impl Default for AdaptiveColor {
fn default() -> Self {
Self::Fixed(Color::rgb(128, 128, 128))
}
}
impl From<Color> for AdaptiveColor {
fn from(color: Color) -> Self {
Self::Fixed(color)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Theme {
pub primary: AdaptiveColor,
pub secondary: AdaptiveColor,
pub accent: AdaptiveColor,
pub background: AdaptiveColor,
pub surface: AdaptiveColor,
pub overlay: AdaptiveColor,
pub text: AdaptiveColor,
pub text_muted: AdaptiveColor,
pub text_subtle: AdaptiveColor,
pub success: AdaptiveColor,
pub warning: AdaptiveColor,
pub error: AdaptiveColor,
pub info: AdaptiveColor,
pub border: AdaptiveColor,
pub border_focused: AdaptiveColor,
pub selection_bg: AdaptiveColor,
pub selection_fg: AdaptiveColor,
pub scrollbar_track: AdaptiveColor,
pub scrollbar_thumb: AdaptiveColor,
}
impl Default for Theme {
fn default() -> Self {
themes::dark()
}
}
impl Theme {
pub fn builder() -> ThemeBuilder {
ThemeBuilder::new()
}
#[must_use]
pub fn detect_dark_mode() -> bool {
Self::detect_dark_mode_from_colorfgbg(env::var("COLORFGBG").ok().as_deref())
}
fn detect_dark_mode_from_colorfgbg(colorfgbg: Option<&str>) -> bool {
if let Some(colorfgbg) = colorfgbg
&& let Some(bg_part) = colorfgbg.split(';').next_back()
&& let Ok(bg) = bg_part.trim().parse::<u8>()
{
return bg != 7 && bg != 15;
}
true
}
#[must_use]
pub fn resolve(&self, is_dark: bool) -> ResolvedTheme {
ResolvedTheme {
primary: self.primary.resolve(is_dark),
secondary: self.secondary.resolve(is_dark),
accent: self.accent.resolve(is_dark),
background: self.background.resolve(is_dark),
surface: self.surface.resolve(is_dark),
overlay: self.overlay.resolve(is_dark),
text: self.text.resolve(is_dark),
text_muted: self.text_muted.resolve(is_dark),
text_subtle: self.text_subtle.resolve(is_dark),
success: self.success.resolve(is_dark),
warning: self.warning.resolve(is_dark),
error: self.error.resolve(is_dark),
info: self.info.resolve(is_dark),
border: self.border.resolve(is_dark),
border_focused: self.border_focused.resolve(is_dark),
selection_bg: self.selection_bg.resolve(is_dark),
selection_fg: self.selection_fg.resolve(is_dark),
scrollbar_track: self.scrollbar_track.resolve(is_dark),
scrollbar_thumb: self.scrollbar_thumb.resolve(is_dark),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ResolvedTheme {
pub primary: Color,
pub secondary: Color,
pub accent: Color,
pub background: Color,
pub surface: Color,
pub overlay: Color,
pub text: Color,
pub text_muted: Color,
pub text_subtle: Color,
pub success: Color,
pub warning: Color,
pub error: Color,
pub info: Color,
pub border: Color,
pub border_focused: Color,
pub selection_bg: Color,
pub selection_fg: Color,
pub scrollbar_track: Color,
pub scrollbar_thumb: Color,
}
#[derive(Debug, Clone)]
#[must_use]
pub struct ThemeBuilder {
theme: Theme,
}
impl ThemeBuilder {
pub fn new() -> Self {
Self {
theme: themes::dark(),
}
}
pub fn from_theme(theme: Theme) -> Self {
Self { theme }
}
pub fn primary(mut self, color: impl Into<AdaptiveColor>) -> Self {
self.theme.primary = color.into();
self
}
pub fn secondary(mut self, color: impl Into<AdaptiveColor>) -> Self {
self.theme.secondary = color.into();
self
}
pub fn accent(mut self, color: impl Into<AdaptiveColor>) -> Self {
self.theme.accent = color.into();
self
}
pub fn background(mut self, color: impl Into<AdaptiveColor>) -> Self {
self.theme.background = color.into();
self
}
pub fn surface(mut self, color: impl Into<AdaptiveColor>) -> Self {
self.theme.surface = color.into();
self
}
pub fn overlay(mut self, color: impl Into<AdaptiveColor>) -> Self {
self.theme.overlay = color.into();
self
}
pub fn text(mut self, color: impl Into<AdaptiveColor>) -> Self {
self.theme.text = color.into();
self
}
pub fn text_muted(mut self, color: impl Into<AdaptiveColor>) -> Self {
self.theme.text_muted = color.into();
self
}
pub fn text_subtle(mut self, color: impl Into<AdaptiveColor>) -> Self {
self.theme.text_subtle = color.into();
self
}
pub fn success(mut self, color: impl Into<AdaptiveColor>) -> Self {
self.theme.success = color.into();
self
}
pub fn warning(mut self, color: impl Into<AdaptiveColor>) -> Self {
self.theme.warning = color.into();
self
}
pub fn error(mut self, color: impl Into<AdaptiveColor>) -> Self {
self.theme.error = color.into();
self
}
pub fn info(mut self, color: impl Into<AdaptiveColor>) -> Self {
self.theme.info = color.into();
self
}
pub fn border(mut self, color: impl Into<AdaptiveColor>) -> Self {
self.theme.border = color.into();
self
}
pub fn border_focused(mut self, color: impl Into<AdaptiveColor>) -> Self {
self.theme.border_focused = color.into();
self
}
pub fn selection_bg(mut self, color: impl Into<AdaptiveColor>) -> Self {
self.theme.selection_bg = color.into();
self
}
pub fn selection_fg(mut self, color: impl Into<AdaptiveColor>) -> Self {
self.theme.selection_fg = color.into();
self
}
pub fn scrollbar_track(mut self, color: impl Into<AdaptiveColor>) -> Self {
self.theme.scrollbar_track = color.into();
self
}
pub fn scrollbar_thumb(mut self, color: impl Into<AdaptiveColor>) -> Self {
self.theme.scrollbar_thumb = color.into();
self
}
pub fn build(self) -> Theme {
self.theme
}
}
impl Default for ThemeBuilder {
fn default() -> Self {
Self::new()
}
}
pub mod themes {
use super::*;
#[must_use]
pub fn default() -> Theme {
dark()
}
#[must_use]
pub fn dark() -> Theme {
Theme {
primary: AdaptiveColor::fixed(Color::rgb(88, 166, 255)), secondary: AdaptiveColor::fixed(Color::rgb(163, 113, 247)), accent: AdaptiveColor::fixed(Color::rgb(255, 123, 114)),
background: AdaptiveColor::fixed(Color::rgb(22, 27, 34)), surface: AdaptiveColor::fixed(Color::rgb(33, 38, 45)), overlay: AdaptiveColor::fixed(Color::rgb(48, 54, 61)),
text: AdaptiveColor::fixed(Color::rgb(230, 237, 243)), text_muted: AdaptiveColor::fixed(Color::rgb(139, 148, 158)), text_subtle: AdaptiveColor::fixed(Color::rgb(110, 118, 129)),
success: AdaptiveColor::fixed(Color::rgb(63, 185, 80)), warning: AdaptiveColor::fixed(Color::rgb(210, 153, 34)), error: AdaptiveColor::fixed(Color::rgb(248, 81, 73)), info: AdaptiveColor::fixed(Color::rgb(88, 166, 255)),
border: AdaptiveColor::fixed(Color::rgb(48, 54, 61)), border_focused: AdaptiveColor::fixed(Color::rgb(88, 166, 255)),
selection_bg: AdaptiveColor::fixed(Color::rgb(56, 139, 253)), selection_fg: AdaptiveColor::fixed(Color::rgb(255, 255, 255)),
scrollbar_track: AdaptiveColor::fixed(Color::rgb(33, 38, 45)),
scrollbar_thumb: AdaptiveColor::fixed(Color::rgb(72, 79, 88)),
}
}
#[must_use]
pub fn light() -> Theme {
Theme {
primary: AdaptiveColor::fixed(Color::rgb(9, 105, 218)), secondary: AdaptiveColor::fixed(Color::rgb(130, 80, 223)), accent: AdaptiveColor::fixed(Color::rgb(207, 34, 46)),
background: AdaptiveColor::fixed(Color::rgb(255, 255, 255)), surface: AdaptiveColor::fixed(Color::rgb(246, 248, 250)), overlay: AdaptiveColor::fixed(Color::rgb(255, 255, 255)),
text: AdaptiveColor::fixed(Color::rgb(31, 35, 40)), text_muted: AdaptiveColor::fixed(Color::rgb(87, 96, 106)), text_subtle: AdaptiveColor::fixed(Color::rgb(140, 149, 159)),
success: AdaptiveColor::fixed(Color::rgb(26, 127, 55)), warning: AdaptiveColor::fixed(Color::rgb(158, 106, 3)), error: AdaptiveColor::fixed(Color::rgb(207, 34, 46)), info: AdaptiveColor::fixed(Color::rgb(9, 105, 218)),
border: AdaptiveColor::fixed(Color::rgb(208, 215, 222)), border_focused: AdaptiveColor::fixed(Color::rgb(9, 105, 218)),
selection_bg: AdaptiveColor::fixed(Color::rgb(221, 244, 255)), selection_fg: AdaptiveColor::fixed(Color::rgb(31, 35, 40)),
scrollbar_track: AdaptiveColor::fixed(Color::rgb(246, 248, 250)),
scrollbar_thumb: AdaptiveColor::fixed(Color::rgb(175, 184, 193)),
}
}
#[must_use]
pub fn nord() -> Theme {
Theme {
primary: AdaptiveColor::fixed(Color::rgb(136, 192, 208)), secondary: AdaptiveColor::fixed(Color::rgb(180, 142, 173)), accent: AdaptiveColor::fixed(Color::rgb(191, 97, 106)),
background: AdaptiveColor::fixed(Color::rgb(46, 52, 64)), surface: AdaptiveColor::fixed(Color::rgb(59, 66, 82)), overlay: AdaptiveColor::fixed(Color::rgb(67, 76, 94)),
text: AdaptiveColor::fixed(Color::rgb(236, 239, 244)), text_muted: AdaptiveColor::fixed(Color::rgb(216, 222, 233)), text_subtle: AdaptiveColor::fixed(Color::rgb(129, 161, 193)),
success: AdaptiveColor::fixed(Color::rgb(163, 190, 140)), warning: AdaptiveColor::fixed(Color::rgb(235, 203, 139)), error: AdaptiveColor::fixed(Color::rgb(191, 97, 106)), info: AdaptiveColor::fixed(Color::rgb(129, 161, 193)),
border: AdaptiveColor::fixed(Color::rgb(76, 86, 106)), border_focused: AdaptiveColor::fixed(Color::rgb(136, 192, 208)),
selection_bg: AdaptiveColor::fixed(Color::rgb(76, 86, 106)), selection_fg: AdaptiveColor::fixed(Color::rgb(236, 239, 244)),
scrollbar_track: AdaptiveColor::fixed(Color::rgb(59, 66, 82)),
scrollbar_thumb: AdaptiveColor::fixed(Color::rgb(76, 86, 106)),
}
}
#[must_use]
pub fn dracula() -> Theme {
Theme {
primary: AdaptiveColor::fixed(Color::rgb(189, 147, 249)), secondary: AdaptiveColor::fixed(Color::rgb(255, 121, 198)), accent: AdaptiveColor::fixed(Color::rgb(139, 233, 253)),
background: AdaptiveColor::fixed(Color::rgb(40, 42, 54)), surface: AdaptiveColor::fixed(Color::rgb(68, 71, 90)), overlay: AdaptiveColor::fixed(Color::rgb(68, 71, 90)),
text: AdaptiveColor::fixed(Color::rgb(248, 248, 242)), text_muted: AdaptiveColor::fixed(Color::rgb(188, 188, 188)), text_subtle: AdaptiveColor::fixed(Color::rgb(98, 114, 164)),
success: AdaptiveColor::fixed(Color::rgb(80, 250, 123)), warning: AdaptiveColor::fixed(Color::rgb(255, 184, 108)), error: AdaptiveColor::fixed(Color::rgb(255, 85, 85)), info: AdaptiveColor::fixed(Color::rgb(139, 233, 253)),
border: AdaptiveColor::fixed(Color::rgb(68, 71, 90)), border_focused: AdaptiveColor::fixed(Color::rgb(189, 147, 249)),
selection_bg: AdaptiveColor::fixed(Color::rgb(68, 71, 90)), selection_fg: AdaptiveColor::fixed(Color::rgb(248, 248, 242)),
scrollbar_track: AdaptiveColor::fixed(Color::rgb(40, 42, 54)),
scrollbar_thumb: AdaptiveColor::fixed(Color::rgb(68, 71, 90)),
}
}
#[must_use]
pub fn solarized_dark() -> Theme {
Theme {
primary: AdaptiveColor::fixed(Color::rgb(38, 139, 210)), secondary: AdaptiveColor::fixed(Color::rgb(108, 113, 196)), accent: AdaptiveColor::fixed(Color::rgb(203, 75, 22)),
background: AdaptiveColor::fixed(Color::rgb(0, 43, 54)), surface: AdaptiveColor::fixed(Color::rgb(7, 54, 66)), overlay: AdaptiveColor::fixed(Color::rgb(7, 54, 66)),
text: AdaptiveColor::fixed(Color::rgb(131, 148, 150)), text_muted: AdaptiveColor::fixed(Color::rgb(101, 123, 131)), text_subtle: AdaptiveColor::fixed(Color::rgb(88, 110, 117)),
success: AdaptiveColor::fixed(Color::rgb(133, 153, 0)), warning: AdaptiveColor::fixed(Color::rgb(181, 137, 0)), error: AdaptiveColor::fixed(Color::rgb(220, 50, 47)), info: AdaptiveColor::fixed(Color::rgb(38, 139, 210)),
border: AdaptiveColor::fixed(Color::rgb(7, 54, 66)), border_focused: AdaptiveColor::fixed(Color::rgb(38, 139, 210)),
selection_bg: AdaptiveColor::fixed(Color::rgb(7, 54, 66)), selection_fg: AdaptiveColor::fixed(Color::rgb(147, 161, 161)),
scrollbar_track: AdaptiveColor::fixed(Color::rgb(0, 43, 54)),
scrollbar_thumb: AdaptiveColor::fixed(Color::rgb(7, 54, 66)),
}
}
#[must_use]
pub fn solarized_light() -> Theme {
Theme {
primary: AdaptiveColor::fixed(Color::rgb(38, 139, 210)), secondary: AdaptiveColor::fixed(Color::rgb(108, 113, 196)), accent: AdaptiveColor::fixed(Color::rgb(203, 75, 22)),
background: AdaptiveColor::fixed(Color::rgb(253, 246, 227)), surface: AdaptiveColor::fixed(Color::rgb(238, 232, 213)), overlay: AdaptiveColor::fixed(Color::rgb(253, 246, 227)),
text: AdaptiveColor::fixed(Color::rgb(101, 123, 131)), text_muted: AdaptiveColor::fixed(Color::rgb(88, 110, 117)), text_subtle: AdaptiveColor::fixed(Color::rgb(147, 161, 161)),
success: AdaptiveColor::fixed(Color::rgb(133, 153, 0)), warning: AdaptiveColor::fixed(Color::rgb(181, 137, 0)), error: AdaptiveColor::fixed(Color::rgb(220, 50, 47)), info: AdaptiveColor::fixed(Color::rgb(38, 139, 210)),
border: AdaptiveColor::fixed(Color::rgb(238, 232, 213)), border_focused: AdaptiveColor::fixed(Color::rgb(38, 139, 210)),
selection_bg: AdaptiveColor::fixed(Color::rgb(238, 232, 213)), selection_fg: AdaptiveColor::fixed(Color::rgb(88, 110, 117)),
scrollbar_track: AdaptiveColor::fixed(Color::rgb(253, 246, 227)),
scrollbar_thumb: AdaptiveColor::fixed(Color::rgb(238, 232, 213)),
}
}
#[must_use]
pub fn monokai() -> Theme {
Theme {
primary: AdaptiveColor::fixed(Color::rgb(102, 217, 239)), secondary: AdaptiveColor::fixed(Color::rgb(174, 129, 255)), accent: AdaptiveColor::fixed(Color::rgb(249, 38, 114)),
background: AdaptiveColor::fixed(Color::rgb(39, 40, 34)), surface: AdaptiveColor::fixed(Color::rgb(60, 61, 54)), overlay: AdaptiveColor::fixed(Color::rgb(60, 61, 54)),
text: AdaptiveColor::fixed(Color::rgb(248, 248, 242)), text_muted: AdaptiveColor::fixed(Color::rgb(189, 189, 189)), text_subtle: AdaptiveColor::fixed(Color::rgb(117, 113, 94)),
success: AdaptiveColor::fixed(Color::rgb(166, 226, 46)), warning: AdaptiveColor::fixed(Color::rgb(230, 219, 116)), error: AdaptiveColor::fixed(Color::rgb(249, 38, 114)), info: AdaptiveColor::fixed(Color::rgb(102, 217, 239)),
border: AdaptiveColor::fixed(Color::rgb(60, 61, 54)), border_focused: AdaptiveColor::fixed(Color::rgb(102, 217, 239)),
selection_bg: AdaptiveColor::fixed(Color::rgb(73, 72, 62)), selection_fg: AdaptiveColor::fixed(Color::rgb(248, 248, 242)),
scrollbar_track: AdaptiveColor::fixed(Color::rgb(39, 40, 34)),
scrollbar_thumb: AdaptiveColor::fixed(Color::rgb(60, 61, 54)),
}
}
#[must_use]
pub fn doom() -> Theme {
Theme {
primary: AdaptiveColor::fixed(Color::rgb(178, 34, 34)), secondary: AdaptiveColor::fixed(Color::rgb(50, 205, 50)), accent: AdaptiveColor::fixed(Color::rgb(255, 255, 0)),
background: AdaptiveColor::fixed(Color::rgb(26, 26, 26)), surface: AdaptiveColor::fixed(Color::rgb(47, 47, 47)), overlay: AdaptiveColor::fixed(Color::rgb(64, 64, 64)),
text: AdaptiveColor::fixed(Color::rgb(211, 211, 211)), text_muted: AdaptiveColor::fixed(Color::rgb(128, 128, 128)), text_subtle: AdaptiveColor::fixed(Color::rgb(105, 105, 105)),
success: AdaptiveColor::fixed(Color::rgb(50, 205, 50)), warning: AdaptiveColor::fixed(Color::rgb(255, 215, 0)), error: AdaptiveColor::fixed(Color::rgb(139, 0, 0)), info: AdaptiveColor::fixed(Color::rgb(65, 105, 225)),
border: AdaptiveColor::fixed(Color::rgb(105, 105, 105)), border_focused: AdaptiveColor::fixed(Color::rgb(178, 34, 34)),
selection_bg: AdaptiveColor::fixed(Color::rgb(139, 0, 0)), selection_fg: AdaptiveColor::fixed(Color::rgb(255, 255, 255)),
scrollbar_track: AdaptiveColor::fixed(Color::rgb(26, 26, 26)),
scrollbar_thumb: AdaptiveColor::fixed(Color::rgb(178, 34, 34)),
}
}
#[must_use]
pub fn quake() -> Theme {
Theme {
primary: AdaptiveColor::fixed(Color::rgb(139, 69, 19)), secondary: AdaptiveColor::fixed(Color::rgb(85, 107, 47)), accent: AdaptiveColor::fixed(Color::rgb(205, 133, 63)),
background: AdaptiveColor::fixed(Color::rgb(28, 28, 28)), surface: AdaptiveColor::fixed(Color::rgb(46, 39, 34)), overlay: AdaptiveColor::fixed(Color::rgb(62, 54, 48)),
text: AdaptiveColor::fixed(Color::rgb(210, 180, 140)), text_muted: AdaptiveColor::fixed(Color::rgb(139, 115, 85)), text_subtle: AdaptiveColor::fixed(Color::rgb(101, 84, 61)),
success: AdaptiveColor::fixed(Color::rgb(85, 107, 47)), warning: AdaptiveColor::fixed(Color::rgb(210, 105, 30)), error: AdaptiveColor::fixed(Color::rgb(128, 0, 0)), info: AdaptiveColor::fixed(Color::rgb(70, 130, 180)),
border: AdaptiveColor::fixed(Color::rgb(93, 64, 55)), border_focused: AdaptiveColor::fixed(Color::rgb(205, 133, 63)),
selection_bg: AdaptiveColor::fixed(Color::rgb(139, 69, 19)), selection_fg: AdaptiveColor::fixed(Color::rgb(255, 222, 173)),
scrollbar_track: AdaptiveColor::fixed(Color::rgb(28, 28, 28)),
scrollbar_thumb: AdaptiveColor::fixed(Color::rgb(139, 69, 19)),
}
}
}
pub struct SharedResolvedTheme {
inner: arc_swap::ArcSwap<ResolvedTheme>,
}
impl SharedResolvedTheme {
pub fn new(theme: ResolvedTheme) -> Self {
Self {
inner: arc_swap::ArcSwap::from_pointee(theme),
}
}
#[inline]
pub fn load(&self) -> ResolvedTheme {
let guard = self.inner.load();
**guard
}
#[inline]
pub fn store(&self, theme: ResolvedTheme) {
self.inner.store(std::sync::Arc::new(theme));
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn adaptive_color_fixed() {
let color = AdaptiveColor::fixed(Color::rgb(255, 0, 0));
assert_eq!(color.resolve(true), Color::rgb(255, 0, 0));
assert_eq!(color.resolve(false), Color::rgb(255, 0, 0));
assert!(!color.is_adaptive());
}
#[test]
fn adaptive_color_adaptive() {
let color = AdaptiveColor::adaptive(
Color::rgb(255, 255, 255), Color::rgb(0, 0, 0), );
assert_eq!(color.resolve(true), Color::rgb(0, 0, 0)); assert_eq!(color.resolve(false), Color::rgb(255, 255, 255)); assert!(color.is_adaptive());
}
#[test]
fn theme_default_is_dark() {
let theme = Theme::default();
let bg = theme.background.resolve(true);
if let Color::Rgb(rgb) = bg {
assert!(rgb.luminance_u8() < 50);
}
}
#[test]
fn theme_light_has_light_background() {
let theme = themes::light();
let bg = theme.background.resolve(false);
if let Color::Rgb(rgb) = bg {
assert!(rgb.luminance_u8() > 200);
}
}
#[test]
fn theme_has_all_slots() {
let theme = Theme::default();
let _ = theme.primary.resolve(true);
let _ = theme.secondary.resolve(true);
let _ = theme.accent.resolve(true);
let _ = theme.background.resolve(true);
let _ = theme.surface.resolve(true);
let _ = theme.overlay.resolve(true);
let _ = theme.text.resolve(true);
let _ = theme.text_muted.resolve(true);
let _ = theme.text_subtle.resolve(true);
let _ = theme.success.resolve(true);
let _ = theme.warning.resolve(true);
let _ = theme.error.resolve(true);
let _ = theme.info.resolve(true);
let _ = theme.border.resolve(true);
let _ = theme.border_focused.resolve(true);
let _ = theme.selection_bg.resolve(true);
let _ = theme.selection_fg.resolve(true);
let _ = theme.scrollbar_track.resolve(true);
let _ = theme.scrollbar_thumb.resolve(true);
}
#[test]
fn theme_builder_works() {
let theme = Theme::builder()
.primary(Color::rgb(255, 0, 0))
.background(Color::rgb(0, 0, 0))
.build();
assert_eq!(theme.primary.resolve(true), Color::rgb(255, 0, 0));
assert_eq!(theme.background.resolve(true), Color::rgb(0, 0, 0));
}
#[test]
fn theme_resolve_flattens() {
let theme = themes::dark();
let resolved = theme.resolve(true);
assert_eq!(resolved.primary, theme.primary.resolve(true));
assert_eq!(resolved.text, theme.text.resolve(true));
assert_eq!(resolved.background, theme.background.resolve(true));
}
#[test]
fn all_presets_exist() {
let _ = themes::default();
let _ = themes::dark();
let _ = themes::light();
let _ = themes::nord();
let _ = themes::dracula();
let _ = themes::solarized_dark();
let _ = themes::solarized_light();
let _ = themes::monokai();
}
#[test]
fn presets_have_different_colors() {
let dark = themes::dark();
let light = themes::light();
let nord = themes::nord();
assert_ne!(
dark.background.resolve(true),
light.background.resolve(false)
);
assert_ne!(dark.background.resolve(true), nord.background.resolve(true));
}
#[test]
fn detect_dark_mode_returns_bool() {
let _ = Theme::detect_dark_mode();
}
#[test]
fn color_converts_to_adaptive() {
let color = Color::rgb(100, 150, 200);
let adaptive: AdaptiveColor = color.into();
assert_eq!(adaptive.resolve(true), color);
assert_eq!(adaptive.resolve(false), color);
}
#[test]
fn builder_from_theme() {
let base = themes::nord();
let modified = ThemeBuilder::from_theme(base.clone())
.primary(Color::rgb(255, 0, 0))
.build();
assert_eq!(modified.primary.resolve(true), Color::rgb(255, 0, 0));
assert_eq!(modified.secondary, base.secondary);
}
#[test]
fn has_at_least_15_semantic_slots() {
let theme = Theme::default();
let slot_count = 19; assert!(slot_count >= 15);
let _slots = [
&theme.primary,
&theme.secondary,
&theme.accent,
&theme.background,
&theme.surface,
&theme.overlay,
&theme.text,
&theme.text_muted,
&theme.text_subtle,
&theme.success,
&theme.warning,
&theme.error,
&theme.info,
&theme.border,
&theme.border_focused,
&theme.selection_bg,
&theme.selection_fg,
&theme.scrollbar_track,
&theme.scrollbar_thumb,
];
}
#[test]
fn adaptive_color_default_is_gray() {
let color = AdaptiveColor::default();
assert!(!color.is_adaptive());
assert_eq!(color.resolve(true), Color::rgb(128, 128, 128));
assert_eq!(color.resolve(false), Color::rgb(128, 128, 128));
}
#[test]
fn theme_builder_default() {
let builder = ThemeBuilder::default();
let theme = builder.build();
assert_eq!(theme, themes::dark());
}
#[test]
fn resolved_theme_has_all_19_slots() {
let theme = themes::dark();
let resolved = theme.resolve(true);
let _colors = [
resolved.primary,
resolved.secondary,
resolved.accent,
resolved.background,
resolved.surface,
resolved.overlay,
resolved.text,
resolved.text_muted,
resolved.text_subtle,
resolved.success,
resolved.warning,
resolved.error,
resolved.info,
resolved.border,
resolved.border_focused,
resolved.selection_bg,
resolved.selection_fg,
resolved.scrollbar_track,
resolved.scrollbar_thumb,
];
}
#[test]
fn dark_and_light_resolve_differently() {
let theme = Theme {
text: AdaptiveColor::adaptive(Color::rgb(0, 0, 0), Color::rgb(255, 255, 255)),
..themes::dark()
};
let dark_resolved = theme.resolve(true);
let light_resolved = theme.resolve(false);
assert_ne!(dark_resolved.text, light_resolved.text);
assert_eq!(dark_resolved.text, Color::rgb(255, 255, 255));
assert_eq!(light_resolved.text, Color::rgb(0, 0, 0));
}
#[test]
fn all_dark_presets_have_dark_backgrounds() {
for (name, theme) in [
("dark", themes::dark()),
("nord", themes::nord()),
("dracula", themes::dracula()),
("solarized_dark", themes::solarized_dark()),
("monokai", themes::monokai()),
] {
let bg = theme.background.resolve(true);
if let Color::Rgb(rgb) = bg {
assert!(
rgb.luminance_u8() < 100,
"{name} background too bright: {}",
rgb.luminance_u8()
);
}
}
}
#[test]
fn all_light_presets_have_light_backgrounds() {
for (name, theme) in [
("light", themes::light()),
("solarized_light", themes::solarized_light()),
] {
let bg = theme.background.resolve(false);
if let Color::Rgb(rgb) = bg {
assert!(
rgb.luminance_u8() > 150,
"{name} background too dark: {}",
rgb.luminance_u8()
);
}
}
}
#[test]
fn theme_default_equals_dark() {
assert_eq!(Theme::default(), themes::dark());
assert_eq!(themes::default(), themes::dark());
}
#[test]
fn builder_all_setters_chain() {
let theme = Theme::builder()
.primary(Color::rgb(1, 0, 0))
.secondary(Color::rgb(2, 0, 0))
.accent(Color::rgb(3, 0, 0))
.background(Color::rgb(4, 0, 0))
.surface(Color::rgb(5, 0, 0))
.overlay(Color::rgb(6, 0, 0))
.text(Color::rgb(7, 0, 0))
.text_muted(Color::rgb(8, 0, 0))
.text_subtle(Color::rgb(9, 0, 0))
.success(Color::rgb(10, 0, 0))
.warning(Color::rgb(11, 0, 0))
.error(Color::rgb(12, 0, 0))
.info(Color::rgb(13, 0, 0))
.border(Color::rgb(14, 0, 0))
.border_focused(Color::rgb(15, 0, 0))
.selection_bg(Color::rgb(16, 0, 0))
.selection_fg(Color::rgb(17, 0, 0))
.scrollbar_track(Color::rgb(18, 0, 0))
.scrollbar_thumb(Color::rgb(19, 0, 0))
.build();
assert_eq!(theme.primary.resolve(true), Color::rgb(1, 0, 0));
assert_eq!(theme.scrollbar_thumb.resolve(true), Color::rgb(19, 0, 0));
}
#[test]
fn resolved_theme_is_copy() {
let theme = themes::dark();
let resolved = theme.resolve(true);
let copy = resolved;
assert_eq!(resolved, copy);
}
#[test]
fn detect_dark_mode_with_colorfgbg_dark() {
let result = Theme::detect_dark_mode_from_colorfgbg(Some("0;0"));
assert!(result, "bg=0 should be dark mode");
}
#[test]
fn detect_dark_mode_with_colorfgbg_light_15() {
let result = Theme::detect_dark_mode_from_colorfgbg(Some("0;15"));
assert!(!result, "bg=15 should be light mode");
}
#[test]
fn detect_dark_mode_with_colorfgbg_light_7() {
let result = Theme::detect_dark_mode_from_colorfgbg(Some("0;7"));
assert!(!result, "bg=7 should be light mode");
}
#[test]
fn detect_dark_mode_without_env_defaults_dark() {
let result = Theme::detect_dark_mode_from_colorfgbg(None);
assert!(result, "missing COLORFGBG should default to dark");
}
#[test]
fn detect_dark_mode_with_empty_string() {
let result = Theme::detect_dark_mode_from_colorfgbg(Some(""));
assert!(result, "empty COLORFGBG should default to dark");
}
#[test]
fn detect_dark_mode_with_no_semicolon() {
let result = Theme::detect_dark_mode_from_colorfgbg(Some("0"));
assert!(result, "COLORFGBG without semicolon should default to dark");
}
#[test]
fn detect_dark_mode_with_multiple_semicolons() {
let result = Theme::detect_dark_mode_from_colorfgbg(Some("0;0;extra"));
assert!(result, "COLORFGBG with extra parts should use last as bg");
}
#[test]
fn detect_dark_mode_with_whitespace() {
let result = Theme::detect_dark_mode_from_colorfgbg(Some("0; 15 "));
assert!(!result, "COLORFGBG with whitespace should parse correctly");
}
#[test]
fn detect_dark_mode_with_invalid_number() {
let result = Theme::detect_dark_mode_from_colorfgbg(Some("0;abc"));
assert!(
result,
"COLORFGBG with invalid number should default to dark"
);
}
#[test]
fn theme_clone_produces_equal_theme() {
let theme = themes::nord();
let cloned = theme.clone();
assert_eq!(theme, cloned);
}
#[test]
fn theme_equality_different_themes() {
let dark = themes::dark();
let light = themes::light();
assert_ne!(dark, light);
}
#[test]
fn resolved_theme_different_modes_differ() {
let theme = Theme {
text: AdaptiveColor::adaptive(Color::rgb(0, 0, 0), Color::rgb(255, 255, 255)),
background: AdaptiveColor::adaptive(Color::rgb(255, 255, 255), Color::rgb(0, 0, 0)),
..themes::dark()
};
let dark_resolved = theme.resolve(true);
let light_resolved = theme.resolve(false);
assert_ne!(dark_resolved, light_resolved);
}
#[test]
fn resolved_theme_equality_same_mode() {
let theme = themes::dark();
let resolved1 = theme.resolve(true);
let resolved2 = theme.resolve(true);
assert_eq!(resolved1, resolved2);
}
#[test]
fn preset_nord_has_characteristic_colors() {
let nord = themes::nord();
let primary = nord.primary.resolve(true);
if let Color::Rgb(rgb) = primary {
assert!(rgb.b > rgb.r, "Nord primary should be bluish");
}
}
#[test]
fn preset_dracula_has_characteristic_colors() {
let dracula = themes::dracula();
let primary = dracula.primary.resolve(true);
if let Color::Rgb(rgb) = primary {
assert!(
rgb.r > 100 && rgb.b > 200,
"Dracula primary should be purple"
);
}
}
#[test]
fn preset_monokai_has_characteristic_colors() {
let monokai = themes::monokai();
let primary = monokai.primary.resolve(true);
if let Color::Rgb(rgb) = primary {
assert!(rgb.g > 200 && rgb.b > 200, "Monokai primary should be cyan");
}
}
#[test]
fn preset_solarized_dark_and_light_share_accent_colors() {
let sol_dark = themes::solarized_dark();
let sol_light = themes::solarized_light();
assert_eq!(
sol_dark.primary.resolve(true),
sol_light.primary.resolve(true),
"Solarized dark and light should share primary accent"
);
}
#[test]
fn builder_accepts_adaptive_color_directly() {
let adaptive = AdaptiveColor::adaptive(Color::rgb(0, 0, 0), Color::rgb(255, 255, 255));
let theme = Theme::builder().text(adaptive).build();
assert!(theme.text.is_adaptive());
}
#[test]
fn all_presets_have_distinct_error_colors_from_info() {
for (name, theme) in [
("dark", themes::dark()),
("light", themes::light()),
("nord", themes::nord()),
("dracula", themes::dracula()),
("solarized_dark", themes::solarized_dark()),
("monokai", themes::monokai()),
] {
let error = theme.error.resolve(true);
let info = theme.info.resolve(true);
assert_ne!(
error, info,
"{name} should have distinct error and info colors"
);
}
}
#[test]
fn adaptive_color_debug_impl() {
let fixed = AdaptiveColor::fixed(Color::rgb(255, 0, 0));
let adaptive = AdaptiveColor::adaptive(Color::rgb(0, 0, 0), Color::rgb(255, 255, 255));
let _ = format!("{:?}", fixed);
let _ = format!("{:?}", adaptive);
}
#[test]
fn theme_debug_impl() {
let theme = themes::dark();
let debug = format!("{:?}", theme);
assert!(debug.contains("Theme"));
}
#[test]
fn resolved_theme_debug_impl() {
let resolved = themes::dark().resolve(true);
let debug = format!("{:?}", resolved);
assert!(debug.contains("ResolvedTheme"));
}
#[test]
fn shared_theme_load_returns_initial() {
let dark = themes::dark().resolve(true);
let shared = SharedResolvedTheme::new(dark);
assert_eq!(shared.load(), dark);
}
#[test]
fn shared_theme_store_replaces_value() {
let original = themes::dark().resolve(true);
let mut updated = original;
updated.primary = Color::rgb(0, 0, 0);
assert_ne!(original.primary, updated.primary);
let shared = SharedResolvedTheme::new(original);
shared.store(updated);
assert_eq!(shared.load(), updated);
assert_ne!(shared.load(), original);
}
#[test]
fn shared_theme_concurrent_read_write() {
use std::sync::{Arc, Barrier};
use std::thread;
let dark = themes::dark().resolve(true);
let light = themes::dark().resolve(false);
let shared = Arc::new(SharedResolvedTheme::new(dark));
let barrier = Arc::new(Barrier::new(5));
let readers: Vec<_> = (0..4)
.map(|_| {
let s = Arc::clone(&shared);
let b = Arc::clone(&barrier);
let dark_copy = dark;
let light_copy = light;
thread::spawn(move || {
b.wait();
for _ in 0..10_000 {
let theme = s.load();
assert!(
theme == dark_copy || theme == light_copy,
"torn read detected"
);
}
})
})
.collect();
let writer = {
let s = Arc::clone(&shared);
let b = Arc::clone(&barrier);
thread::spawn(move || {
b.wait();
for i in 0..1_000 {
if i % 2 == 0 {
s.store(light);
} else {
s.store(dark);
}
}
})
};
writer.join().unwrap();
for h in readers {
h.join().unwrap();
}
}
}