use ratatui::style::Color;
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct Theme {
pub text: Color,
pub accent: Color,
pub result_accent: Color,
pub focus: Color,
pub success: Color,
pub info: Color,
pub warning: Color,
pub metadata: Color,
pub border: Color,
pub shell_border: Color,
pub panel_focus_border: Color,
pub error: Color,
pub dim: Color,
pub input_bg: Color,
pub shell_bg: Color,
pub sidebar_bg: Color,
pub workspace_bg: Color,
pub focus_bg: Color,
pub panel_bg: Color,
pub surface_raised: Color,
pub header_bg: Color,
pub selection_bg: Color,
pub selection_fg: Color,
pub selected_idle_bg: Color,
pub selected_idle_fg: Color,
pub pill_bg: Color,
pub badge_bg: Color,
pub primary_action_bg: Color,
pub primary_action_fg: Color,
pub secondary_action_bg: Color,
pub secondary_action_fg: Color,
pub preview_bg: Color,
pub overlay_bg: Color,
pub divider: Color,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ThemePreset {
CalmDark,
HighContrastDark,
Light,
}
impl Theme {
#[must_use]
#[allow(clippy::too_many_lines)]
pub fn from_preset(preset: ThemePreset) -> Self {
match preset {
ThemePreset::CalmDark => Self {
text: Color::Rgb(214, 225, 228),
accent: Color::Rgb(67, 225, 232),
result_accent: Color::Rgb(102, 236, 242),
focus: Color::Rgb(198, 245, 248),
success: Color::Rgb(110, 214, 154),
info: Color::Rgb(109, 198, 210),
warning: Color::Rgb(232, 186, 92),
metadata: Color::Rgb(132, 160, 166),
border: Color::Rgb(36, 72, 79),
shell_border: Color::Rgb(25, 49, 56),
panel_focus_border: Color::Rgb(102, 236, 242),
error: Color::Rgb(255, 99, 110),
dim: Color::Rgb(92, 116, 122),
input_bg: Color::Rgb(15, 34, 38),
shell_bg: Color::Rgb(5, 14, 17),
sidebar_bg: Color::Rgb(8, 20, 24),
workspace_bg: Color::Rgb(10, 24, 29),
focus_bg: Color::Rgb(18, 58, 66),
panel_bg: Color::Rgb(10, 24, 29),
surface_raised: Color::Rgb(17, 47, 53),
header_bg: Color::Rgb(14, 47, 54),
selection_bg: Color::Rgb(12, 88, 97),
selection_fg: Color::Rgb(245, 251, 252),
selected_idle_bg: Color::Rgb(12, 61, 68),
selected_idle_fg: Color::Rgb(230, 243, 245),
pill_bg: Color::Rgb(12, 28, 32),
badge_bg: Color::Rgb(16, 39, 44),
primary_action_bg: Color::Rgb(46, 126, 134),
primary_action_fg: Color::Rgb(5, 14, 17),
secondary_action_bg: Color::Rgb(16, 39, 44),
secondary_action_fg: Color::Rgb(188, 208, 212),
preview_bg: Color::Rgb(8, 20, 24),
overlay_bg: Color::Rgb(12, 26, 31),
divider: Color::Rgb(28, 58, 64),
},
ThemePreset::HighContrastDark => Self {
text: Color::Rgb(245, 247, 250),
accent: Color::Rgb(102, 218, 194),
result_accent: Color::Rgb(245, 185, 96),
focus: Color::Rgb(210, 225, 236),
success: Color::Rgb(125, 229, 171),
info: Color::Rgb(136, 201, 222),
warning: Color::Rgb(242, 195, 102),
metadata: Color::Rgb(175, 188, 202),
border: Color::Rgb(90, 106, 122),
shell_border: Color::Rgb(62, 75, 90),
panel_focus_border: Color::Rgb(136, 201, 222),
error: Color::Rgb(255, 99, 110),
dim: Color::Rgb(128, 142, 156),
input_bg: Color::Rgb(26, 34, 42),
shell_bg: Color::Rgb(10, 15, 22),
sidebar_bg: Color::Rgb(16, 22, 30),
workspace_bg: Color::Rgb(20, 28, 36),
focus_bg: Color::Rgb(44, 64, 78),
panel_bg: Color::Rgb(20, 26, 34),
surface_raised: Color::Rgb(30, 40, 50),
header_bg: Color::Rgb(14, 20, 26),
selection_bg: Color::Rgb(44, 64, 78),
selection_fg: Color::Rgb(245, 247, 250),
selected_idle_bg: Color::Rgb(30, 40, 50),
selected_idle_fg: Color::Rgb(245, 247, 250),
pill_bg: Color::Rgb(18, 26, 34),
badge_bg: Color::Rgb(23, 31, 40),
primary_action_bg: Color::Rgb(74, 146, 158),
primary_action_fg: Color::Rgb(20, 26, 34),
secondary_action_bg: Color::Rgb(34, 45, 56),
secondary_action_fg: Color::Rgb(206, 216, 226),
preview_bg: Color::Rgb(14, 20, 26),
overlay_bg: Color::Rgb(18, 25, 33),
divider: Color::Rgb(72, 88, 102),
},
ThemePreset::Light => Self {
text: Color::Rgb(24, 32, 40),
accent: Color::Rgb(34, 149, 132),
result_accent: Color::Rgb(168, 110, 36),
focus: Color::Rgb(90, 122, 142),
success: Color::Rgb(49, 148, 96),
info: Color::Rgb(70, 120, 150),
warning: Color::Rgb(160, 113, 25),
metadata: Color::Rgb(96, 108, 120),
border: Color::Rgb(180, 188, 196),
shell_border: Color::Rgb(204, 210, 218),
panel_focus_border: Color::Rgb(34, 149, 132),
error: Color::Rgb(199, 58, 71),
dim: Color::Rgb(130, 140, 150),
input_bg: Color::Rgb(243, 246, 250),
shell_bg: Color::Rgb(236, 240, 244),
sidebar_bg: Color::Rgb(242, 246, 250),
workspace_bg: Color::Rgb(250, 252, 255),
focus_bg: Color::Rgb(223, 233, 243),
panel_bg: Color::Rgb(248, 250, 252),
surface_raised: Color::Rgb(238, 243, 248),
header_bg: Color::Rgb(238, 242, 246),
selection_bg: Color::Rgb(223, 233, 243),
selection_fg: Color::Rgb(24, 32, 40),
selected_idle_bg: Color::Rgb(238, 243, 248),
selected_idle_fg: Color::Rgb(24, 32, 40),
pill_bg: Color::Rgb(235, 240, 245),
badge_bg: Color::Rgb(231, 237, 243),
primary_action_bg: Color::Rgb(52, 138, 128),
primary_action_fg: Color::Rgb(248, 250, 252),
secondary_action_bg: Color::Rgb(226, 233, 239),
secondary_action_fg: Color::Rgb(56, 70, 84),
preview_bg: Color::Rgb(238, 242, 246),
overlay_bg: Color::Rgb(244, 247, 251),
divider: Color::Rgb(200, 208, 216),
},
}
}
}
impl Default for Theme {
fn default() -> Self {
Theme::from_preset(ThemePreset::CalmDark)
}
}
#[cfg(test)]
mod tests {
use super::{Theme, ThemePreset};
#[test]
fn presets_keep_secondary_text_and_focus_roles_distinct() {
for preset in [
ThemePreset::CalmDark,
ThemePreset::HighContrastDark,
ThemePreset::Light,
] {
let theme = Theme::from_preset(preset);
assert_ne!(theme.metadata, theme.dim);
assert_ne!(theme.focus, theme.panel_focus_border);
}
}
#[test]
fn calm_dark_uses_distinct_result_accent() {
let theme = Theme::from_preset(ThemePreset::CalmDark);
assert_ne!(theme.accent, theme.result_accent);
}
#[test]
fn presets_keep_primary_action_stronger_than_passive_hint_roles() {
for preset in [
ThemePreset::CalmDark,
ThemePreset::HighContrastDark,
ThemePreset::Light,
] {
let theme = Theme::from_preset(preset);
assert_ne!(theme.primary_action_bg, theme.badge_bg);
assert_ne!(theme.primary_action_bg, theme.shell_bg);
assert_ne!(theme.secondary_action_fg, theme.dim);
}
}
#[test]
fn presets_keep_primary_action_distinct_from_success_feedback() {
for preset in [
ThemePreset::CalmDark,
ThemePreset::HighContrastDark,
ThemePreset::Light,
] {
let theme = Theme::from_preset(preset);
assert_ne!(theme.primary_action_bg, theme.success);
}
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct Keymap {
pub help: char,
pub search: char,
}
impl Default for Keymap {
fn default() -> Self {
Self {
help: '?',
search: '/',
}
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct LayoutConfig {
pub sidebar_ratio: u16,
}
impl Default for LayoutConfig {
fn default() -> Self {
Self { sidebar_ratio: 24 }
}
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct TuiConfig {
pub theme: Theme,
pub keymap: Keymap,
pub start_command: Option<String>,
pub layout: LayoutConfig,
}