use std::collections::HashMap;
use ratatui::style::{Color, Style};
use tuillem_config::ThemeColors;
#[derive(Debug, Clone)]
pub struct Theme {
pub bg: Color,
pub fg: Color,
pub sidebar_bg: Color,
pub sidebar_fg: Color,
pub sidebar_selected: Color,
pub user_msg_bg: Color,
pub assistant_msg_bg: Color,
pub thinking_fg: Color,
pub accent: Color,
pub error: Color,
pub success: Color,
pub warning: Color,
pub border: Color,
pub code_bg: Color,
pub code_fg: Color,
pub heading: Color,
pub link: Color,
pub tag: Color,
pub sidebar_selected_bg: Color,
}
impl Theme {
pub fn dark() -> Self {
Self {
bg: Color::Rgb(30, 30, 46), fg: Color::Rgb(205, 214, 244), sidebar_bg: Color::Rgb(24, 24, 37), sidebar_fg: Color::Rgb(186, 194, 222), sidebar_selected: Color::Rgb(137, 180, 250), user_msg_bg: Color::Rgb(49, 50, 68), assistant_msg_bg: Color::Rgb(30, 30, 46), thinking_fg: Color::Rgb(127, 132, 156), accent: Color::Rgb(137, 180, 250), error: Color::Rgb(243, 139, 168), success: Color::Rgb(166, 227, 161), warning: Color::Rgb(249, 226, 175), border: Color::Rgb(88, 91, 112), code_bg: Color::Rgb(24, 24, 37), code_fg: Color::Rgb(166, 227, 161), heading: Color::Rgb(180, 190, 254), link: Color::Rgb(116, 199, 236), tag: Color::Rgb(203, 166, 247), sidebar_selected_bg: Color::Rgb(49, 50, 68), }
}
pub fn light() -> Self {
Self {
bg: Color::Rgb(239, 241, 245), fg: Color::Rgb(76, 79, 105), sidebar_bg: Color::Rgb(230, 233, 239), sidebar_fg: Color::Rgb(92, 95, 119), sidebar_selected: Color::Rgb(30, 102, 245), user_msg_bg: Color::Rgb(204, 208, 218), assistant_msg_bg: Color::Rgb(239, 241, 245), thinking_fg: Color::Rgb(140, 143, 161), accent: Color::Rgb(30, 102, 245), error: Color::Rgb(210, 15, 57), success: Color::Rgb(64, 160, 43), warning: Color::Rgb(223, 142, 29), border: Color::Rgb(172, 176, 190), code_bg: Color::Rgb(230, 233, 239), code_fg: Color::Rgb(64, 160, 43), heading: Color::Rgb(114, 135, 253), link: Color::Rgb(4, 165, 229), tag: Color::Rgb(136, 57, 239), sidebar_selected_bg: Color::Rgb(188, 192, 204), }
}
pub fn dracula() -> Self {
Self {
bg: Color::Rgb(40, 42, 54), fg: Color::Rgb(248, 248, 242), sidebar_bg: Color::Rgb(33, 34, 44), sidebar_fg: Color::Rgb(248, 248, 242), sidebar_selected: Color::Rgb(189, 147, 249), user_msg_bg: Color::Rgb(68, 71, 90), assistant_msg_bg: Color::Rgb(40, 42, 54), thinking_fg: Color::Rgb(98, 114, 164), accent: Color::Rgb(189, 147, 249), error: Color::Rgb(255, 85, 85), success: Color::Rgb(80, 250, 123), warning: Color::Rgb(241, 250, 140), border: Color::Rgb(68, 71, 90), code_bg: Color::Rgb(33, 34, 44), code_fg: Color::Rgb(80, 250, 123), heading: Color::Rgb(255, 121, 198), link: Color::Rgb(139, 233, 253), tag: Color::Rgb(189, 147, 249), sidebar_selected_bg: Color::Rgb(68, 71, 90), }
}
pub fn nord() -> Self {
Self {
bg: Color::Rgb(46, 52, 64), fg: Color::Rgb(216, 222, 233), sidebar_bg: Color::Rgb(59, 66, 82), sidebar_fg: Color::Rgb(216, 222, 233), sidebar_selected: Color::Rgb(136, 192, 208), user_msg_bg: Color::Rgb(67, 76, 94), assistant_msg_bg: Color::Rgb(46, 52, 64), thinking_fg: Color::Rgb(76, 86, 106), accent: Color::Rgb(136, 192, 208), error: Color::Rgb(191, 97, 106), success: Color::Rgb(163, 190, 140), warning: Color::Rgb(235, 203, 139), border: Color::Rgb(76, 86, 106), code_bg: Color::Rgb(59, 66, 82), code_fg: Color::Rgb(163, 190, 140), heading: Color::Rgb(129, 161, 193), link: Color::Rgb(136, 192, 208), tag: Color::Rgb(180, 142, 173), sidebar_selected_bg: Color::Rgb(76, 86, 106), }
}
pub fn gruvbox() -> Self {
Self {
bg: Color::Rgb(40, 40, 40), fg: Color::Rgb(235, 219, 178), sidebar_bg: Color::Rgb(29, 32, 33), sidebar_fg: Color::Rgb(235, 219, 178), sidebar_selected: Color::Rgb(250, 189, 47), user_msg_bg: Color::Rgb(60, 56, 54), assistant_msg_bg: Color::Rgb(40, 40, 40), thinking_fg: Color::Rgb(146, 131, 116), accent: Color::Rgb(250, 189, 47), error: Color::Rgb(251, 73, 52), success: Color::Rgb(184, 187, 38), warning: Color::Rgb(254, 128, 25), border: Color::Rgb(80, 73, 69), code_bg: Color::Rgb(29, 32, 33), code_fg: Color::Rgb(184, 187, 38), heading: Color::Rgb(131, 165, 152), link: Color::Rgb(131, 165, 152), tag: Color::Rgb(211, 134, 155), sidebar_selected_bg: Color::Rgb(80, 73, 69), }
}
pub fn tokyo_night() -> Self {
Self {
bg: Color::Rgb(26, 27, 38), fg: Color::Rgb(169, 177, 214), sidebar_bg: Color::Rgb(22, 22, 30), sidebar_fg: Color::Rgb(169, 177, 214), sidebar_selected: Color::Rgb(122, 162, 247), user_msg_bg: Color::Rgb(41, 46, 66), assistant_msg_bg: Color::Rgb(26, 27, 38), thinking_fg: Color::Rgb(86, 95, 137), accent: Color::Rgb(122, 162, 247), error: Color::Rgb(247, 118, 142), success: Color::Rgb(158, 206, 106), warning: Color::Rgb(224, 175, 104), border: Color::Rgb(41, 46, 66), code_bg: Color::Rgb(22, 22, 30), code_fg: Color::Rgb(158, 206, 106), heading: Color::Rgb(187, 154, 247), link: Color::Rgb(125, 207, 255), tag: Color::Rgb(187, 154, 247), sidebar_selected_bg: Color::Rgb(54, 58, 79), }
}
pub fn solarized() -> Self {
Self {
bg: Color::Rgb(0, 43, 54), fg: Color::Rgb(131, 148, 150), sidebar_bg: Color::Rgb(7, 54, 66), sidebar_fg: Color::Rgb(131, 148, 150), sidebar_selected: Color::Rgb(38, 139, 210), user_msg_bg: Color::Rgb(7, 54, 66), assistant_msg_bg: Color::Rgb(0, 43, 54), thinking_fg: Color::Rgb(88, 110, 117), accent: Color::Rgb(38, 139, 210), error: Color::Rgb(220, 50, 47), success: Color::Rgb(133, 153, 0), warning: Color::Rgb(181, 137, 0), border: Color::Rgb(88, 110, 117), code_bg: Color::Rgb(7, 54, 66), code_fg: Color::Rgb(133, 153, 0), heading: Color::Rgb(108, 113, 196), link: Color::Rgb(42, 161, 152), tag: Color::Rgb(108, 113, 196), sidebar_selected_bg: Color::Rgb(0, 54, 66), }
}
pub fn solarized_light() -> Self {
Self {
bg: Color::Rgb(253, 246, 227), fg: Color::Rgb(101, 123, 131), sidebar_bg: Color::Rgb(238, 232, 213), sidebar_fg: Color::Rgb(101, 123, 131), sidebar_selected: Color::Rgb(38, 139, 210), user_msg_bg: Color::Rgb(238, 232, 213), assistant_msg_bg: Color::Rgb(253, 246, 227), thinking_fg: Color::Rgb(147, 161, 161), accent: Color::Rgb(38, 139, 210), error: Color::Rgb(220, 50, 47), success: Color::Rgb(133, 153, 0), warning: Color::Rgb(181, 137, 0), border: Color::Rgb(147, 161, 161), code_bg: Color::Rgb(238, 232, 213), code_fg: Color::Rgb(42, 161, 152), heading: Color::Rgb(108, 113, 196), link: Color::Rgb(42, 161, 152), tag: Color::Rgb(211, 54, 130), sidebar_selected_bg: Color::Rgb(253, 246, 227), }
}
pub fn github_light() -> Self {
Self {
bg: Color::Rgb(255, 255, 255), fg: Color::Rgb(31, 35, 40), sidebar_bg: Color::Rgb(246, 248, 250), sidebar_fg: Color::Rgb(31, 35, 40), sidebar_selected: Color::Rgb(9, 105, 218), user_msg_bg: Color::Rgb(221, 244, 255), assistant_msg_bg: Color::Rgb(255, 255, 255), thinking_fg: Color::Rgb(101, 109, 118), accent: Color::Rgb(9, 105, 218), error: Color::Rgb(207, 34, 46), success: Color::Rgb(26, 127, 55), warning: Color::Rgb(154, 103, 0), border: Color::Rgb(208, 215, 222), code_bg: Color::Rgb(246, 248, 250), code_fg: Color::Rgb(26, 127, 55), heading: Color::Rgb(9, 105, 218), link: Color::Rgb(9, 105, 218), tag: Color::Rgb(130, 80, 223), sidebar_selected_bg: Color::Rgb(221, 244, 255), }
}
pub fn rose_pine_dawn() -> Self {
Self {
bg: Color::Rgb(250, 244, 237), fg: Color::Rgb(87, 82, 121), sidebar_bg: Color::Rgb(255, 250, 243), sidebar_fg: Color::Rgb(87, 82, 121), sidebar_selected: Color::Rgb(40, 105, 131), user_msg_bg: Color::Rgb(242, 233, 222), assistant_msg_bg: Color::Rgb(250, 244, 237), thinking_fg: Color::Rgb(152, 147, 165), accent: Color::Rgb(40, 105, 131), error: Color::Rgb(180, 99, 122), success: Color::Rgb(40, 105, 131), warning: Color::Rgb(234, 157, 52), border: Color::Rgb(206, 202, 205), code_bg: Color::Rgb(242, 233, 222), code_fg: Color::Rgb(87, 82, 121), heading: Color::Rgb(144, 122, 169), link: Color::Rgb(40, 105, 131), tag: Color::Rgb(144, 122, 169), sidebar_selected_bg: Color::Rgb(242, 233, 222), }
}
pub fn monokai_pro() -> Self {
Self {
bg: Color::Rgb(45, 42, 46), fg: Color::Rgb(252, 252, 248), sidebar_bg: Color::Rgb(34, 32, 35), sidebar_fg: Color::Rgb(200, 200, 196), sidebar_selected: Color::Rgb(255, 216, 102), user_msg_bg: Color::Rgb(57, 53, 58), assistant_msg_bg: Color::Rgb(45, 42, 46), thinking_fg: Color::Rgb(114, 113, 115), accent: Color::Rgb(255, 216, 102), error: Color::Rgb(255, 97, 136), success: Color::Rgb(169, 220, 118), warning: Color::Rgb(252, 152, 103), border: Color::Rgb(73, 69, 74), code_bg: Color::Rgb(34, 32, 35), code_fg: Color::Rgb(169, 220, 118), heading: Color::Rgb(171, 157, 242), link: Color::Rgb(120, 220, 232), tag: Color::Rgb(171, 157, 242), sidebar_selected_bg: Color::Rgb(57, 53, 58), }
}
pub fn everforest() -> Self {
Self {
bg: Color::Rgb(47, 53, 55), fg: Color::Rgb(211, 198, 170), sidebar_bg: Color::Rgb(38, 44, 46), sidebar_fg: Color::Rgb(211, 198, 170), sidebar_selected: Color::Rgb(163, 190, 140), user_msg_bg: Color::Rgb(58, 64, 66), assistant_msg_bg: Color::Rgb(47, 53, 55), thinking_fg: Color::Rgb(133, 146, 137), accent: Color::Rgb(167, 192, 128), error: Color::Rgb(230, 126, 128), success: Color::Rgb(167, 192, 128), warning: Color::Rgb(219, 188, 127), border: Color::Rgb(78, 86, 88), code_bg: Color::Rgb(38, 44, 46), code_fg: Color::Rgb(167, 192, 128), heading: Color::Rgb(214, 153, 182), link: Color::Rgb(126, 196, 193), tag: Color::Rgb(214, 153, 182), sidebar_selected_bg: Color::Rgb(58, 64, 66), }
}
pub fn kanagawa() -> Self {
Self {
bg: Color::Rgb(31, 31, 40), fg: Color::Rgb(220, 215, 186), sidebar_bg: Color::Rgb(22, 22, 29), sidebar_fg: Color::Rgb(200, 195, 167), sidebar_selected: Color::Rgb(127, 180, 202), user_msg_bg: Color::Rgb(54, 54, 70), assistant_msg_bg: Color::Rgb(31, 31, 40), thinking_fg: Color::Rgb(114, 113, 105), accent: Color::Rgb(127, 180, 202), error: Color::Rgb(195, 64, 67), success: Color::Rgb(152, 187, 108), warning: Color::Rgb(255, 169, 73), border: Color::Rgb(84, 84, 109), code_bg: Color::Rgb(22, 22, 29), code_fg: Color::Rgb(152, 187, 108), heading: Color::Rgb(157, 124, 216), link: Color::Rgb(127, 180, 202), tag: Color::Rgb(157, 124, 216), sidebar_selected_bg: Color::Rgb(54, 54, 70), }
}
pub fn one_dark() -> Self {
Self {
bg: Color::Rgb(40, 44, 52), fg: Color::Rgb(171, 178, 191), sidebar_bg: Color::Rgb(33, 37, 43), sidebar_fg: Color::Rgb(171, 178, 191), sidebar_selected: Color::Rgb(97, 175, 239), user_msg_bg: Color::Rgb(50, 56, 66), assistant_msg_bg: Color::Rgb(40, 44, 52), thinking_fg: Color::Rgb(92, 99, 112), accent: Color::Rgb(97, 175, 239), error: Color::Rgb(224, 108, 117), success: Color::Rgb(152, 195, 121), warning: Color::Rgb(229, 192, 123), border: Color::Rgb(62, 68, 81), code_bg: Color::Rgb(33, 37, 43), code_fg: Color::Rgb(152, 195, 121), heading: Color::Rgb(198, 120, 221), link: Color::Rgb(86, 182, 194), tag: Color::Rgb(198, 120, 221), sidebar_selected_bg: Color::Rgb(50, 56, 66), }
}
pub fn ayu_dark() -> Self {
Self {
bg: Color::Rgb(10, 14, 20), fg: Color::Rgb(179, 177, 173), sidebar_bg: Color::Rgb(5, 8, 13), sidebar_fg: Color::Rgb(179, 177, 173), sidebar_selected: Color::Rgb(57, 186, 230), user_msg_bg: Color::Rgb(22, 27, 34), assistant_msg_bg: Color::Rgb(10, 14, 20), thinking_fg: Color::Rgb(94, 101, 111), accent: Color::Rgb(57, 186, 230), error: Color::Rgb(255, 51, 51), success: Color::Rgb(186, 230, 126), warning: Color::Rgb(255, 180, 84), border: Color::Rgb(39, 46, 56), code_bg: Color::Rgb(5, 8, 13), code_fg: Color::Rgb(186, 230, 126), heading: Color::Rgb(217, 191, 255), link: Color::Rgb(89, 199, 255), tag: Color::Rgb(217, 191, 255), sidebar_selected_bg: Color::Rgb(22, 27, 34), }
}
pub fn rose_pine() -> Self {
Self {
bg: Color::Rgb(25, 23, 36), fg: Color::Rgb(224, 222, 244), sidebar_bg: Color::Rgb(18, 17, 28), sidebar_fg: Color::Rgb(224, 222, 244), sidebar_selected: Color::Rgb(156, 207, 216), user_msg_bg: Color::Rgb(38, 35, 53), assistant_msg_bg: Color::Rgb(25, 23, 36), thinking_fg: Color::Rgb(110, 106, 134), accent: Color::Rgb(156, 207, 216), error: Color::Rgb(235, 111, 146), success: Color::Rgb(156, 207, 216), warning: Color::Rgb(246, 193, 119), border: Color::Rgb(57, 53, 82), code_bg: Color::Rgb(18, 17, 28), code_fg: Color::Rgb(224, 222, 244), heading: Color::Rgb(196, 167, 231), link: Color::Rgb(156, 207, 216), tag: Color::Rgb(196, 167, 231), sidebar_selected_bg: Color::Rgb(38, 35, 53), }
}
pub fn everforest_light() -> Self {
Self {
bg: Color::Rgb(253, 246, 227), fg: Color::Rgb(92, 107, 99), sidebar_bg: Color::Rgb(239, 239, 224), sidebar_fg: Color::Rgb(92, 107, 99), sidebar_selected: Color::Rgb(53, 165, 78), user_msg_bg: Color::Rgb(226, 226, 204), assistant_msg_bg: Color::Rgb(253, 246, 227), thinking_fg: Color::Rgb(163, 174, 166), accent: Color::Rgb(53, 165, 78), error: Color::Rgb(243, 113, 93), success: Color::Rgb(143, 191, 96), warning: Color::Rgb(223, 166, 78), border: Color::Rgb(195, 195, 175), code_bg: Color::Rgb(239, 239, 224), code_fg: Color::Rgb(92, 107, 99), heading: Color::Rgb(181, 126, 162), link: Color::Rgb(53, 165, 78), tag: Color::Rgb(181, 126, 162), sidebar_selected_bg: Color::Rgb(226, 226, 204), }
}
pub fn ayu_light() -> Self {
Self {
bg: Color::Rgb(250, 250, 250), fg: Color::Rgb(92, 104, 108), sidebar_bg: Color::Rgb(240, 240, 240), sidebar_fg: Color::Rgb(92, 104, 108), sidebar_selected: Color::Rgb(255, 157, 0), user_msg_bg: Color::Rgb(229, 232, 233), assistant_msg_bg: Color::Rgb(250, 250, 250), thinking_fg: Color::Rgb(171, 179, 182), accent: Color::Rgb(255, 157, 0), error: Color::Rgb(240, 113, 120), success: Color::Rgb(134, 179, 0), warning: Color::Rgb(250, 138, 46), border: Color::Rgb(209, 215, 217), code_bg: Color::Rgb(240, 240, 240), code_fg: Color::Rgb(134, 179, 0), heading: Color::Rgb(163, 122, 204), link: Color::Rgb(57, 186, 230), tag: Color::Rgb(163, 122, 204), sidebar_selected_bg: Color::Rgb(229, 232, 233), }
}
pub fn one_light() -> Self {
Self {
bg: Color::Rgb(250, 250, 250), fg: Color::Rgb(56, 58, 66), sidebar_bg: Color::Rgb(240, 240, 240), sidebar_fg: Color::Rgb(56, 58, 66), sidebar_selected: Color::Rgb(64, 120, 242), user_msg_bg: Color::Rgb(228, 228, 230), assistant_msg_bg: Color::Rgb(250, 250, 250), thinking_fg: Color::Rgb(160, 161, 167), accent: Color::Rgb(64, 120, 242), error: Color::Rgb(228, 86, 73), success: Color::Rgb(80, 161, 79), warning: Color::Rgb(193, 132, 1), border: Color::Rgb(202, 202, 206), code_bg: Color::Rgb(240, 240, 240), code_fg: Color::Rgb(80, 161, 79), heading: Color::Rgb(166, 38, 164), link: Color::Rgb(1, 132, 188), tag: Color::Rgb(166, 38, 164), sidebar_selected_bg: Color::Rgb(228, 228, 230), }
}
pub fn gruvbox_light() -> Self {
Self {
bg: Color::Rgb(251, 241, 199), fg: Color::Rgb(60, 56, 54), sidebar_bg: Color::Rgb(242, 229, 188), sidebar_fg: Color::Rgb(60, 56, 54), sidebar_selected: Color::Rgb(215, 153, 33), user_msg_bg: Color::Rgb(235, 219, 178), assistant_msg_bg: Color::Rgb(251, 241, 199), thinking_fg: Color::Rgb(168, 153, 132), accent: Color::Rgb(215, 153, 33), error: Color::Rgb(204, 36, 29), success: Color::Rgb(152, 151, 26), warning: Color::Rgb(214, 93, 14), border: Color::Rgb(189, 174, 147), code_bg: Color::Rgb(242, 229, 188), code_fg: Color::Rgb(60, 56, 54), heading: Color::Rgb(69, 133, 136), link: Color::Rgb(69, 133, 136), tag: Color::Rgb(177, 98, 134), sidebar_selected_bg: Color::Rgb(235, 219, 178), }
}
pub fn builtin_names() -> &'static [&'static str] {
&[
"dark",
"light",
"dracula",
"nord",
"gruvbox",
"gruvbox_light",
"tokyo_night",
"solarized",
"solarized_light",
"github_light",
"rose_pine",
"rose_pine_dawn",
"monokai_pro",
"everforest",
"everforest_light",
"kanagawa",
"one_dark",
"one_light",
"ayu_dark",
"ayu_light",
]
}
pub fn from_config(name: &str, custom_themes: &HashMap<String, ThemeColors>) -> Self {
let base = match name {
"light" => Self::light(),
"dracula" => Self::dracula(),
"nord" => Self::nord(),
"gruvbox" => Self::gruvbox(),
"gruvbox_light" => Self::gruvbox_light(),
"tokyo_night" => Self::tokyo_night(),
"solarized" => Self::solarized(),
"solarized_light" => Self::solarized_light(),
"github_light" => Self::github_light(),
"rose_pine" => Self::rose_pine(),
"rose_pine_dawn" => Self::rose_pine_dawn(),
"monokai_pro" => Self::monokai_pro(),
"everforest" => Self::everforest(),
"everforest_light" => Self::everforest_light(),
"kanagawa" => Self::kanagawa(),
"one_dark" => Self::one_dark(),
"one_light" => Self::one_light(),
"ayu_dark" => Self::ayu_dark(),
"ayu_light" => Self::ayu_light(),
_ => Self::dark(),
};
match custom_themes.get(name) {
Some(colors) => base.apply_overrides(colors),
None => base,
}
}
pub fn apply_overrides(mut self, colors: &ThemeColors) -> Self {
if let Some(ref c) = colors.bg {
self.bg = parse_hex(c);
}
if let Some(ref c) = colors.fg {
self.fg = parse_hex(c);
}
if let Some(ref c) = colors.sidebar_bg {
self.sidebar_bg = parse_hex(c);
}
if let Some(ref c) = colors.sidebar_fg {
self.sidebar_fg = parse_hex(c);
}
if let Some(ref c) = colors.sidebar_selected {
self.sidebar_selected = parse_hex(c);
}
if let Some(ref c) = colors.user_msg_bg {
self.user_msg_bg = parse_hex(c);
}
if let Some(ref c) = colors.assistant_msg_bg {
self.assistant_msg_bg = parse_hex(c);
}
if let Some(ref c) = colors.thinking_fg {
self.thinking_fg = parse_hex(c);
}
if let Some(ref c) = colors.accent {
self.accent = parse_hex(c);
}
if let Some(ref c) = colors.error {
self.error = parse_hex(c);
}
if let Some(ref c) = colors.success {
self.success = parse_hex(c);
}
if let Some(ref c) = colors.warning {
self.warning = parse_hex(c);
}
if let Some(ref c) = colors.border {
self.border = parse_hex(c);
}
if let Some(ref c) = colors.code_bg {
self.code_bg = parse_hex(c);
}
if let Some(ref c) = colors.code_fg {
self.code_fg = parse_hex(c);
}
if let Some(ref c) = colors.heading {
self.heading = parse_hex(c);
}
if let Some(ref c) = colors.link {
self.link = parse_hex(c);
}
if let Some(ref c) = colors.tag {
self.tag = parse_hex(c);
}
if let Some(ref c) = colors.sidebar_selected_bg {
self.sidebar_selected_bg = parse_hex(c);
}
self
}
pub fn adapt_to_color_mode(&self, mode: &str) -> Self {
Self {
bg: adapt_color(self.bg, mode),
fg: adapt_color(self.fg, mode),
sidebar_bg: adapt_color(self.sidebar_bg, mode),
sidebar_fg: adapt_color(self.sidebar_fg, mode),
sidebar_selected: adapt_color(self.sidebar_selected, mode),
user_msg_bg: adapt_color(self.user_msg_bg, mode),
assistant_msg_bg: adapt_color(self.assistant_msg_bg, mode),
thinking_fg: adapt_color(self.thinking_fg, mode),
accent: adapt_color(self.accent, mode),
error: adapt_color(self.error, mode),
success: adapt_color(self.success, mode),
warning: adapt_color(self.warning, mode),
border: adapt_color(self.border, mode),
code_bg: adapt_color(self.code_bg, mode),
code_fg: adapt_color(self.code_fg, mode),
heading: adapt_color(self.heading, mode),
link: adapt_color(self.link, mode),
tag: adapt_color(self.tag, mode),
sidebar_selected_bg: adapt_color(self.sidebar_selected_bg, mode),
}
}
pub fn sidebar_style(&self) -> Style {
Style::default().fg(self.sidebar_fg).bg(self.sidebar_bg)
}
pub fn sidebar_selected_style(&self) -> Style {
Style::default()
.fg(self.sidebar_selected)
.bg(self.sidebar_selected_bg)
}
pub fn user_message_style(&self) -> Style {
Style::default().fg(self.fg).bg(self.user_msg_bg)
}
pub fn assistant_message_style(&self) -> Style {
Style::default().fg(self.fg).bg(self.assistant_msg_bg)
}
pub fn thinking_style(&self) -> Style {
Style::default().fg(self.thinking_fg)
}
pub fn border_style(&self) -> Style {
Style::default().fg(self.border)
}
pub fn error_style(&self) -> Style {
Style::default().fg(self.error)
}
}
pub fn detect_color_mode() -> &'static str {
if let Ok(ct) = std::env::var("COLORTERM")
&& (ct == "truecolor" || ct == "24bit")
{
return "truecolor";
}
if let Ok(term) = std::env::var("TERM")
&& term.contains("256color")
{
return "256";
}
"basic"
}
pub fn resolve_color_mode(mode: &str) -> &str {
match mode {
"truecolor" | "256" | "basic" => mode,
_ => detect_color_mode(), }
}
pub fn adapt_color(color: Color, mode: &str) -> Color {
match mode {
"truecolor" => color,
"256" => rgb_to_256(color),
"basic" => rgb_to_basic(color),
_ => color, }
}
fn rgb_to_256(color: Color) -> Color {
if let Color::Rgb(r, g, b) = color {
if r == g && g == b {
if r < 8 {
return Color::Indexed(16);
}
if r > 248 {
return Color::Indexed(231);
}
return Color::Indexed(232 + ((r as u16 - 8) * 24 / 247) as u8);
}
let ri = (r as u16 * 5 / 255) as u8;
let gi = (g as u16 * 5 / 255) as u8;
let bi = (b as u16 * 5 / 255) as u8;
Color::Indexed(16 + 36 * ri + 6 * gi + bi)
} else {
color
}
}
fn rgb_to_basic(color: Color) -> Color {
if let Color::Rgb(r, g, b) = color {
let luma = ((r as u32 * 299 + g as u32 * 587 + b as u32 * 114) / 1000) as u16;
let bright = luma > 127;
let max = r.max(g).max(b);
let min = r.min(g).min(b);
let chroma = max as i16 - min as i16;
if chroma < 30 {
return match luma {
0..=63 => Color::Black,
64..=127 => Color::DarkGray,
128..=191 => Color::Gray,
_ => Color::White,
};
}
match (r >= g, r >= b, g >= b) {
(true, true, true) if g as i16 - b as i16 > chroma / 3 => {
if bright {
Color::LightYellow
} else {
Color::Yellow
}
}
(true, true, _) => {
if bright {
Color::LightRed
} else {
Color::Red
}
}
(false, _, true) if g as i16 - r as i16 > chroma / 3 => {
if bright {
Color::LightGreen
} else {
Color::Green
}
}
(false, _, true) => {
if bright {
Color::LightCyan
} else {
Color::Cyan
}
}
(_, false, false) if r as i16 - g as i16 > chroma / 3 => {
if bright {
Color::LightMagenta
} else {
Color::Magenta
}
}
(_, false, false) => {
if bright {
Color::LightBlue
} else {
Color::Blue
}
}
_ => {
if bright {
Color::White
} else {
Color::Gray
}
}
}
} else {
color
}
}
pub fn parse_hex(hex: &str) -> Color {
let hex = hex.strip_prefix('#').unwrap_or(hex);
if hex.len() != 6 {
return Color::White;
}
let r = u8::from_str_radix(&hex[0..2], 16);
let g = u8::from_str_radix(&hex[2..4], 16);
let b = u8::from_str_radix(&hex[4..6], 16);
match (r, g, b) {
(Ok(r), Ok(g), Ok(b)) => Color::Rgb(r, g, b),
_ => Color::White,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_hex() {
assert_eq!(parse_hex("#ff0000"), Color::Rgb(255, 0, 0));
assert_eq!(parse_hex("00ff00"), Color::Rgb(0, 255, 0));
assert_eq!(parse_hex("#1e1e2e"), Color::Rgb(30, 30, 46));
assert_eq!(parse_hex("zzzzzz"), Color::White);
assert_eq!(parse_hex("short"), Color::White);
assert_eq!(parse_hex(""), Color::White);
}
#[test]
fn test_dark_theme() {
let theme = Theme::dark();
assert_eq!(theme.bg, Color::Rgb(30, 30, 46));
assert_eq!(theme.fg, Color::Rgb(205, 214, 244));
assert_eq!(theme.sidebar_bg, Color::Rgb(24, 24, 37));
}
#[test]
fn test_rgb_to_256_greyscale() {
assert_eq!(rgb_to_256(Color::Rgb(0, 0, 0)), Color::Indexed(16));
assert_eq!(rgb_to_256(Color::Rgb(255, 255, 255)), Color::Indexed(231));
let c = rgb_to_256(Color::Rgb(128, 128, 128));
if let Color::Indexed(i) = c {
assert!(
(232..=255).contains(&i),
"Expected greyscale index, got {i}"
);
} else {
panic!("Expected Color::Indexed");
}
}
#[test]
fn test_rgb_to_256_colour() {
let c = rgb_to_256(Color::Rgb(255, 0, 0));
assert_eq!(c, Color::Indexed(16 + 36 * 5)); }
#[test]
fn test_rgb_to_basic() {
assert_eq!(rgb_to_basic(Color::Rgb(255, 255, 255)), Color::White);
assert_eq!(rgb_to_basic(Color::Rgb(0, 0, 0)), Color::Black);
assert_eq!(rgb_to_basic(Color::Red), Color::Red);
assert_eq!(rgb_to_basic(Color::Reset), Color::Reset);
}
#[test]
fn test_adapt_color_passthrough() {
let c = Color::Rgb(100, 200, 50);
assert_eq!(adapt_color(c, "truecolor"), c);
assert_eq!(adapt_color(Color::Red, "256"), Color::Red);
assert_eq!(adapt_color(Color::Reset, "basic"), Color::Reset);
}
#[test]
fn test_adapt_to_color_mode() {
let theme = Theme::dark();
let adapted = theme.adapt_to_color_mode("256");
if let Color::Indexed(_) = adapted.bg {
} else {
panic!("Expected bg to be Color::Indexed after 256 adaptation");
}
}
#[test]
fn test_custom_theme_override() {
let mut custom = HashMap::new();
custom.insert(
"dark".to_string(),
ThemeColors {
bg: Some("#000000".to_string()),
fg: Some("#ffffff".to_string()),
..Default::default()
},
);
let theme = Theme::from_config("dark", &custom);
assert_eq!(theme.bg, Color::Rgb(0, 0, 0));
assert_eq!(theme.fg, Color::Rgb(255, 255, 255));
assert_eq!(theme.sidebar_bg, Color::Rgb(24, 24, 37));
}
}