mod resolve;
pub use resolve::{AppearanceArg, ThemeArg, resolve_theme_with_config};
use std::sync::OnceLock;
use ratatui::style::Color;
use two_face::theme::EmbeddedThemeName;
use travelagent_core::syntax::SyntaxHighlighter;
pub struct Theme {
highlighter: OnceLock<SyntaxHighlighter>,
pub panel_bg: Color,
pub bg_highlight: Color,
pub fg_primary: Color,
pub fg_secondary: Color,
pub fg_dim: Color,
pub diff_add: Color,
pub diff_add_bg: Color,
pub diff_del: Color,
pub diff_del_bg: Color,
pub diff_context: Color,
pub diff_hunk_header: Color,
pub expanded_context_fg: Color,
pub syntax_add_bg: Color,
pub syntax_del_bg: Color,
pub syntect_theme: EmbeddedThemeName,
pub file_added: Color,
pub file_modified: Color,
pub file_deleted: Color,
pub file_renamed: Color,
pub reviewed: Color,
pub pending: Color,
pub comment_note: Color,
pub comment_suggestion: Color,
pub comment_issue: Color,
pub comment_praise: Color,
pub comment_question: Color,
pub border_focused: Color,
pub border_unfocused: Color,
pub status_bar_bg: Color,
pub cursor_color: Color,
pub branch_name: Color,
pub help_indicator: Color,
pub message_info_fg: Color,
pub message_info_bg: Color,
pub message_warning_fg: Color,
pub message_warning_bg: Color,
pub message_error_fg: Color,
pub message_error_bg: Color,
pub update_badge_fg: Color,
pub update_badge_bg: Color,
pub mode_fg: Color,
pub mode_bg: Color,
pub markdown_header: Color,
pub markdown_code: Color,
pub markdown_code_bg: Color,
pub markdown_link: Color,
pub markdown_blockquote: Color,
pub markdown_rule: Color,
pub file_ref: Color,
pub risk_low: Color,
pub risk_medium: Color,
pub risk_high: Color,
}
impl Default for Theme {
fn default() -> Self {
Self::dark()
}
}
impl Theme {
pub fn dark() -> Self {
Self {
highlighter: OnceLock::new(),
panel_bg: Color::Rgb(24, 24, 28),
bg_highlight: Color::Rgb(70, 70, 70),
fg_primary: Color::White,
fg_secondary: Color::Rgb(210, 210, 210),
fg_dim: Color::Rgb(160, 160, 160),
diff_add: Color::Rgb(125, 180, 229),
diff_add_bg: Color::Rgb(10, 30, 55),
diff_del: Color::Rgb(242, 153, 74),
diff_del_bg: Color::Rgb(55, 30, 10),
diff_context: Color::Rgb(200, 200, 200),
diff_hunk_header: Color::Rgb(90, 200, 255),
expanded_context_fg: Color::Rgb(140, 140, 140),
syntax_add_bg: Color::Rgb(6, 20, 38),
syntax_del_bg: Color::Rgb(38, 20, 6),
syntect_theme: EmbeddedThemeName::Base16EightiesDark,
file_added: Color::Rgb(80, 220, 120),
file_modified: Color::Rgb(255, 210, 90),
file_deleted: Color::Rgb(240, 90, 90),
file_renamed: Color::Rgb(255, 140, 220),
reviewed: Color::Rgb(80, 220, 120),
pending: Color::Rgb(255, 210, 90),
comment_note: Color::Rgb(90, 170, 255),
comment_suggestion: Color::Rgb(90, 220, 240),
comment_issue: Color::Rgb(240, 90, 90),
comment_praise: Color::Rgb(80, 220, 120),
comment_question: Color::Rgb(220, 180, 60),
border_focused: Color::Rgb(90, 200, 255),
border_unfocused: Color::Rgb(110, 110, 110),
status_bar_bg: Color::Rgb(30, 30, 30),
cursor_color: Color::Rgb(255, 210, 90),
branch_name: Color::Rgb(90, 220, 240),
help_indicator: Color::Rgb(110, 110, 110),
message_info_fg: Color::Black,
message_info_bg: Color::Cyan,
message_warning_fg: Color::Black,
message_warning_bg: Color::Rgb(255, 210, 90),
message_error_fg: Color::White,
message_error_bg: Color::Rgb(240, 90, 90),
update_badge_fg: Color::Black,
update_badge_bg: Color::Rgb(255, 210, 90),
mode_fg: Color::Black,
mode_bg: Color::Rgb(90, 200, 255),
markdown_header: Color::Rgb(90, 200, 255),
markdown_code: Color::Rgb(220, 220, 140),
markdown_code_bg: Color::Rgb(40, 40, 46),
markdown_link: Color::Rgb(90, 170, 255),
markdown_blockquote: Color::Rgb(160, 160, 160),
markdown_rule: Color::Rgb(110, 110, 110),
file_ref: Color::Rgb(90, 220, 240),
risk_low: Color::Rgb(80, 220, 120),
risk_medium: Color::Rgb(255, 210, 90),
risk_high: Color::Rgb(240, 90, 90),
}
}
pub fn light() -> Self {
Self {
highlighter: OnceLock::new(),
panel_bg: Color::Rgb(245, 243, 232),
bg_highlight: Color::Rgb(200, 200, 220),
fg_primary: Color::Rgb(0, 0, 0),
fg_secondary: Color::Rgb(30, 30, 30),
fg_dim: Color::Rgb(80, 80, 80),
diff_add: Color::Rgb(31, 92, 175), diff_add_bg: Color::Rgb(222, 236, 252), diff_del: Color::Rgb(184, 101, 30), diff_del_bg: Color::Rgb(252, 236, 220), diff_context: Color::Rgb(0, 0, 0), diff_hunk_header: Color::Rgb(0, 60, 140),
expanded_context_fg: Color::Rgb(60, 60, 60),
syntax_add_bg: Color::Rgb(222, 236, 252),
syntax_del_bg: Color::Rgb(252, 236, 220),
syntect_theme: EmbeddedThemeName::Base16OceanLight,
file_added: Color::Rgb(0, 100, 0),
file_modified: Color::Rgb(140, 80, 0),
file_deleted: Color::Rgb(160, 0, 0),
file_renamed: Color::Rgb(100, 0, 100),
reviewed: Color::Rgb(0, 100, 0),
pending: Color::Rgb(140, 80, 0),
comment_note: Color::Rgb(0, 60, 140),
comment_suggestion: Color::Rgb(0, 100, 120),
comment_issue: Color::Rgb(160, 0, 0),
comment_praise: Color::Rgb(0, 100, 0),
comment_question: Color::Rgb(160, 120, 0),
border_focused: Color::Rgb(0, 60, 140),
border_unfocused: Color::Rgb(100, 100, 100),
status_bar_bg: Color::Rgb(210, 210, 220),
cursor_color: Color::Rgb(140, 80, 0),
branch_name: Color::Rgb(0, 100, 120),
help_indicator: Color::Rgb(90, 90, 90),
message_info_fg: Color::Black,
message_info_bg: Color::Rgb(140, 220, 255),
message_warning_fg: Color::Black,
message_warning_bg: Color::Rgb(240, 210, 150),
message_error_fg: Color::White,
message_error_bg: Color::Rgb(180, 60, 60),
update_badge_fg: Color::Black,
update_badge_bg: Color::Rgb(240, 210, 150),
mode_fg: Color::White,
mode_bg: Color::Rgb(0, 80, 160),
markdown_header: Color::Rgb(0, 60, 140),
markdown_code: Color::Rgb(120, 70, 0),
markdown_code_bg: Color::Rgb(232, 232, 220),
markdown_link: Color::Rgb(0, 60, 140),
markdown_blockquote: Color::Rgb(80, 80, 80),
markdown_rule: Color::Rgb(120, 120, 120),
file_ref: Color::Rgb(0, 100, 120),
risk_low: Color::Rgb(0, 100, 0),
risk_medium: Color::Rgb(140, 80, 0),
risk_high: Color::Rgb(160, 0, 0),
}
}
pub fn solarized_light() -> Self {
let base03 = Color::Rgb(0, 43, 54);
let base01 = Color::Rgb(88, 110, 117);
let base00 = Color::Rgb(101, 123, 131);
let base1 = Color::Rgb(147, 161, 161);
let base2 = Color::Rgb(238, 232, 213);
let base3 = Color::Rgb(253, 246, 227);
let yellow = Color::Rgb(181, 137, 0);
let orange = Color::Rgb(203, 75, 22);
let red = Color::Rgb(220, 50, 47);
let violet = Color::Rgb(108, 113, 196);
let blue = Color::Rgb(38, 139, 210);
let cyan = Color::Rgb(42, 161, 152);
let green = Color::Rgb(133, 153, 0);
Self {
highlighter: OnceLock::new(),
panel_bg: base3,
bg_highlight: base2,
fg_primary: base00,
fg_secondary: base01,
fg_dim: base1,
diff_add: Color::Rgb(0, 80, 0),
diff_add_bg: Color::Rgb(222, 240, 205),
diff_del: Color::Rgb(140, 0, 0),
diff_del_bg: Color::Rgb(252, 225, 224),
diff_context: base00,
diff_hunk_header: blue,
expanded_context_fg: base1,
syntax_add_bg: Color::Rgb(222, 240, 205),
syntax_del_bg: Color::Rgb(252, 225, 224),
syntect_theme: EmbeddedThemeName::SolarizedLight,
file_added: green,
file_modified: yellow,
file_deleted: red,
file_renamed: violet,
reviewed: green,
pending: yellow,
comment_note: blue,
comment_suggestion: cyan,
comment_issue: red,
comment_praise: green,
comment_question: yellow,
border_focused: blue,
border_unfocused: base1,
status_bar_bg: base2,
cursor_color: orange,
branch_name: cyan,
help_indicator: base01,
message_info_fg: base3,
message_info_bg: blue,
message_warning_fg: base03,
message_warning_bg: yellow,
message_error_fg: base3,
message_error_bg: red,
update_badge_fg: base03,
update_badge_bg: yellow,
mode_fg: base3,
mode_bg: blue,
markdown_header: blue,
markdown_code: yellow,
markdown_code_bg: base2,
markdown_link: blue,
markdown_blockquote: base01,
markdown_rule: base1,
file_ref: cyan,
risk_low: green,
risk_medium: yellow,
risk_high: red,
}
}
pub fn solarized_dark() -> Self {
let base03 = Color::Rgb(0, 43, 54);
let base02 = Color::Rgb(7, 54, 66);
let base01 = Color::Rgb(88, 110, 117);
let base00 = Color::Rgb(101, 123, 131);
let base0 = Color::Rgb(131, 148, 150);
let base3 = Color::Rgb(253, 246, 227);
let yellow = Color::Rgb(181, 137, 0);
let orange = Color::Rgb(203, 75, 22);
let red = Color::Rgb(220, 50, 47);
let violet = Color::Rgb(108, 113, 196);
let blue = Color::Rgb(38, 139, 210);
let cyan = Color::Rgb(42, 161, 152);
let green = Color::Rgb(133, 153, 0);
Self {
highlighter: OnceLock::new(),
panel_bg: base03,
bg_highlight: base02,
fg_primary: base0,
fg_secondary: base00,
fg_dim: base01,
diff_add: Color::Rgb(80, 220, 120),
diff_add_bg: Color::Rgb(0, 60, 20),
diff_del: Color::Rgb(240, 90, 90),
diff_del_bg: Color::Rgb(70, 0, 0),
diff_context: base0,
diff_hunk_header: blue,
expanded_context_fg: base01,
syntax_add_bg: Color::Rgb(0, 60, 20),
syntax_del_bg: Color::Rgb(70, 0, 0),
syntect_theme: EmbeddedThemeName::SolarizedDark,
file_added: green,
file_modified: yellow,
file_deleted: red,
file_renamed: violet,
reviewed: green,
pending: yellow,
comment_note: blue,
comment_suggestion: cyan,
comment_issue: red,
comment_praise: green,
comment_question: yellow,
border_focused: blue,
border_unfocused: base01,
status_bar_bg: base02,
cursor_color: orange,
branch_name: cyan,
help_indicator: base00,
message_info_fg: base03,
message_info_bg: blue,
message_warning_fg: base03,
message_warning_bg: yellow,
message_error_fg: base3,
message_error_bg: red,
update_badge_fg: base03,
update_badge_bg: yellow,
mode_fg: base3,
mode_bg: blue,
markdown_header: blue,
markdown_code: yellow,
markdown_code_bg: base02,
markdown_link: blue,
markdown_blockquote: base01,
markdown_rule: base01,
file_ref: cyan,
risk_low: green,
risk_medium: yellow,
risk_high: red,
}
}
pub fn catppuccin_latte() -> Self {
let flavor = CatppuccinFlavor {
dark: false,
text: rgb(76, 79, 105),
subtext1: rgb(92, 95, 119),
overlay1: rgb(140, 143, 161),
overlay0: rgb(156, 160, 176),
surface2: rgb(172, 176, 190),
surface1: rgb(188, 192, 204),
base: rgb(239, 241, 245),
mantle: rgb(230, 233, 239),
crust: rgb(220, 224, 232),
red: rgb(210, 15, 57),
yellow: rgb(223, 142, 29),
green: rgb(64, 160, 43),
teal: rgb(23, 146, 153),
blue: rgb(30, 102, 245),
lavender: rgb(114, 135, 253),
peach: rgb(254, 100, 11),
pink: rgb(234, 118, 203),
};
catppuccin_theme(flavor, EmbeddedThemeName::CatppuccinLatte)
}
pub fn catppuccin_frappe() -> Self {
let flavor = CatppuccinFlavor {
dark: true,
text: rgb(198, 208, 245),
subtext1: rgb(181, 191, 226),
overlay1: rgb(131, 139, 167),
overlay0: rgb(115, 121, 148),
surface2: rgb(98, 104, 128),
surface1: rgb(81, 87, 109),
base: rgb(48, 52, 70),
mantle: rgb(41, 44, 60),
crust: rgb(35, 38, 52),
red: rgb(231, 130, 132),
yellow: rgb(229, 200, 144),
green: rgb(166, 209, 137),
teal: rgb(129, 200, 190),
blue: rgb(140, 170, 238),
lavender: rgb(186, 187, 241),
peach: rgb(239, 159, 118),
pink: rgb(244, 184, 228),
};
catppuccin_theme(flavor, EmbeddedThemeName::CatppuccinFrappe)
}
pub fn catppuccin_macchiato() -> Self {
let flavor = CatppuccinFlavor {
dark: true,
text: rgb(202, 211, 245),
subtext1: rgb(184, 192, 224),
overlay1: rgb(128, 135, 162),
overlay0: rgb(110, 115, 141),
surface2: rgb(91, 96, 120),
surface1: rgb(73, 77, 100),
base: rgb(36, 39, 58),
mantle: rgb(30, 32, 48),
crust: rgb(24, 25, 38),
red: rgb(237, 135, 150),
yellow: rgb(238, 212, 159),
green: rgb(166, 218, 149),
teal: rgb(139, 213, 202),
blue: rgb(138, 173, 244),
lavender: rgb(183, 189, 248),
peach: rgb(245, 169, 127),
pink: rgb(245, 189, 230),
};
catppuccin_theme(flavor, EmbeddedThemeName::CatppuccinMacchiato)
}
pub fn catppuccin_mocha() -> Self {
let flavor = CatppuccinFlavor {
dark: true,
text: rgb(205, 214, 244),
subtext1: rgb(186, 194, 222),
overlay1: rgb(127, 132, 156),
overlay0: rgb(108, 112, 134),
surface2: rgb(88, 91, 112),
surface1: rgb(69, 71, 90),
base: rgb(30, 30, 46),
mantle: rgb(24, 24, 37),
crust: rgb(17, 17, 27),
red: rgb(243, 139, 168),
yellow: rgb(249, 226, 175),
green: rgb(166, 227, 161),
teal: rgb(148, 226, 213),
blue: rgb(137, 180, 250),
lavender: rgb(180, 190, 254),
peach: rgb(250, 179, 135),
pink: rgb(245, 194, 231),
};
catppuccin_theme(flavor, EmbeddedThemeName::CatppuccinMocha)
}
pub fn ayu_light() -> Self {
Self {
highlighter: OnceLock::new(),
panel_bg: Color::Rgb(250, 250, 250),
bg_highlight: Color::Rgb(240, 238, 228),
fg_primary: Color::Rgb(92, 103, 115),
fg_secondary: Color::Rgb(107, 118, 130),
fg_dim: Color::Rgb(171, 176, 182),
diff_add: Color::Rgb(134, 179, 0),
diff_add_bg: Color::Rgb(238, 247, 208),
diff_del: Color::Rgb(240, 113, 120),
diff_del_bg: Color::Rgb(253, 235, 236),
diff_context: Color::Rgb(92, 103, 115),
diff_hunk_header: Color::Rgb(54, 163, 217),
expanded_context_fg: Color::Rgb(130, 140, 153),
syntax_add_bg: Color::Rgb(244, 251, 228),
syntax_del_bg: Color::Rgb(255, 241, 242),
syntect_theme: EmbeddedThemeName::OneHalfLight,
file_added: Color::Rgb(134, 179, 0),
file_modified: Color::Rgb(231, 197, 71),
file_deleted: Color::Rgb(240, 113, 120),
file_renamed: Color::Rgb(163, 122, 204),
reviewed: Color::Rgb(134, 179, 0),
pending: Color::Rgb(231, 197, 71),
comment_note: Color::Rgb(54, 163, 217),
comment_suggestion: Color::Rgb(76, 191, 153),
comment_issue: Color::Rgb(240, 113, 120),
comment_praise: Color::Rgb(134, 179, 0),
comment_question: Color::Rgb(219, 171, 9),
border_focused: Color::Rgb(54, 163, 217),
border_unfocused: Color::Rgb(217, 216, 215),
status_bar_bg: Color::Rgb(255, 255, 255),
cursor_color: Color::Rgb(255, 106, 0),
branch_name: Color::Rgb(54, 163, 217),
help_indicator: Color::Rgb(171, 176, 182),
message_info_fg: Color::Black,
message_info_bg: Color::Rgb(140, 220, 255),
message_warning_fg: Color::Black,
message_warning_bg: Color::Rgb(246, 217, 140),
message_error_fg: Color::White,
message_error_bg: Color::Rgb(217, 87, 87),
update_badge_fg: Color::Black,
update_badge_bg: Color::Rgb(246, 217, 140),
mode_fg: Color::White,
mode_bg: Color::Rgb(255, 106, 0),
markdown_header: Color::Rgb(54, 163, 217),
markdown_code: Color::Rgb(150, 100, 0),
markdown_code_bg: Color::Rgb(240, 238, 228),
markdown_link: Color::Rgb(54, 163, 217),
markdown_blockquote: Color::Rgb(130, 140, 153),
markdown_rule: Color::Rgb(171, 176, 182),
file_ref: Color::Rgb(76, 191, 153),
risk_low: Color::Rgb(134, 179, 0),
risk_medium: Color::Rgb(231, 197, 71),
risk_high: Color::Rgb(240, 113, 120),
}
}
pub fn onedark() -> Self {
Self {
highlighter: OnceLock::new(),
panel_bg: Color::Rgb(40, 44, 52),
bg_highlight: Color::Rgb(62, 68, 82),
fg_primary: Color::Rgb(171, 178, 191),
fg_secondary: Color::Rgb(192, 198, 208),
fg_dim: Color::Rgb(92, 99, 112),
diff_add: Color::Rgb(152, 195, 121),
diff_add_bg: Color::Rgb(44, 56, 43),
diff_del: Color::Rgb(224, 108, 117),
diff_del_bg: Color::Rgb(58, 45, 47),
diff_context: Color::Rgb(171, 178, 191),
diff_hunk_header: Color::Rgb(86, 182, 194),
expanded_context_fg: Color::Rgb(92, 99, 112),
syntax_add_bg: Color::Rgb(37, 49, 38),
syntax_del_bg: Color::Rgb(59, 37, 40),
syntect_theme: EmbeddedThemeName::OneHalfDark,
file_added: Color::Rgb(152, 195, 121),
file_modified: Color::Rgb(229, 192, 123),
file_deleted: Color::Rgb(224, 108, 117),
file_renamed: Color::Rgb(198, 120, 221),
reviewed: Color::Rgb(152, 195, 121),
pending: Color::Rgb(229, 192, 123),
comment_note: Color::Rgb(97, 175, 239),
comment_suggestion: Color::Rgb(86, 182, 194),
comment_issue: Color::Rgb(224, 108, 117),
comment_praise: Color::Rgb(152, 195, 121),
comment_question: Color::Rgb(229, 192, 123),
border_focused: Color::Rgb(97, 175, 239),
border_unfocused: Color::Rgb(62, 68, 82),
status_bar_bg: Color::Rgb(33, 37, 43),
cursor_color: Color::Rgb(229, 192, 123),
branch_name: Color::Rgb(86, 182, 194),
help_indicator: Color::Rgb(92, 99, 112),
message_info_fg: Color::Black,
message_info_bg: Color::Rgb(86, 182, 194),
message_warning_fg: Color::Black,
message_warning_bg: Color::Rgb(229, 192, 123),
message_error_fg: Color::White,
message_error_bg: Color::Rgb(224, 108, 117),
update_badge_fg: Color::Black,
update_badge_bg: Color::Rgb(229, 192, 123),
mode_fg: Color::Rgb(40, 44, 52),
mode_bg: Color::Rgb(97, 175, 239),
markdown_header: Color::Rgb(97, 175, 239),
markdown_code: Color::Rgb(229, 192, 123),
markdown_code_bg: Color::Rgb(55, 60, 70),
markdown_link: Color::Rgb(97, 175, 239),
markdown_blockquote: Color::Rgb(92, 99, 112),
markdown_rule: Color::Rgb(92, 99, 112),
file_ref: Color::Rgb(86, 182, 194),
risk_low: Color::Rgb(152, 195, 121),
risk_medium: Color::Rgb(229, 192, 123),
risk_high: Color::Rgb(224, 108, 117),
}
}
pub fn gruvbox_dark() -> Self {
let flavor = GruvboxFlavor {
dark: true,
bg0: rgb(29, 32, 33),
bg1: rgb(40, 40, 40),
bg4: rgb(80, 73, 69),
selected_bg: rgb(60, 56, 54),
fg0: rgb(212, 190, 152),
fg1: rgb(221, 199, 161),
grey0: rgb(124, 111, 100),
grey1: rgb(146, 131, 116),
red: rgb(251, 73, 52),
orange: rgb(254, 128, 25),
yellow: rgb(250, 189, 47),
green: rgb(184, 187, 38),
aqua: rgb(142, 192, 124),
blue: rgb(131, 165, 152),
purple: rgb(211, 134, 155),
bg_red: rgb(64, 33, 32),
bg_green: rgb(52, 56, 27),
};
gruvbox_theme(flavor)
}
pub fn gruvbox_light() -> Self {
let flavor = GruvboxFlavor {
dark: false,
bg0: rgb(249, 245, 215),
bg1: rgb(245, 237, 202),
bg4: rgb(221, 199, 161),
selected_bg: rgb(235, 219, 178),
fg0: rgb(101, 71, 53),
fg1: rgb(79, 56, 41),
grey0: rgb(168, 153, 132),
grey1: rgb(146, 131, 116),
red: rgb(157, 0, 6),
orange: rgb(175, 58, 3),
yellow: rgb(181, 118, 20),
green: rgb(121, 116, 14),
aqua: rgb(66, 123, 88),
blue: rgb(7, 102, 120),
purple: rgb(143, 63, 113),
bg_red: rgb(240, 222, 222),
bg_green: rgb(228, 236, 213),
};
gruvbox_theme(flavor)
}
pub fn nord_dark() -> Self {
nord_theme(NordFlavor {
dark: true,
bg0: rgb(46, 52, 64), bg1: rgb(59, 66, 82), bg2: rgb(67, 76, 94), bg3: rgb(76, 86, 106), fg0: rgb(216, 222, 233), fg1: rgb(229, 233, 240), frost0: rgb(143, 188, 187), frost1: rgb(136, 192, 208), frost2: rgb(129, 161, 193), red: rgb(191, 97, 106), orange: rgb(208, 135, 112), yellow: rgb(235, 203, 139), green: rgb(163, 190, 140), syntect_theme: EmbeddedThemeName::Nord,
})
}
pub fn nord_light() -> Self {
nord_theme(NordFlavor {
dark: false,
bg0: rgb(236, 239, 244), bg1: rgb(229, 233, 240), bg2: rgb(216, 222, 233), bg3: rgb(76, 86, 106), fg0: rgb(46, 52, 64), fg1: rgb(59, 66, 82), frost0: rgb(143, 188, 187), frost1: rgb(136, 192, 208), frost2: rgb(129, 161, 193), red: rgb(191, 97, 106), orange: rgb(208, 135, 112), yellow: rgb(235, 203, 139), green: rgb(163, 190, 140), syntect_theme: EmbeddedThemeName::Base16OceanLight,
})
}
pub fn nord_dark_high_contrast() -> Self {
nord_theme(NordFlavor {
dark: true,
bg0: rgb(46, 52, 64), bg1: rgb(59, 66, 82), bg2: rgb(67, 76, 94), bg3: rgb(76, 86, 106), fg0: rgb(236, 239, 244), fg1: rgb(229, 233, 240), frost0: rgb(143, 188, 187), frost1: rgb(136, 192, 208), frost2: rgb(129, 161, 193), red: rgb(191, 97, 106), orange: rgb(208, 135, 112), yellow: rgb(235, 203, 139), green: rgb(163, 190, 140), syntect_theme: EmbeddedThemeName::Nord,
})
}
pub fn nord_light_high_contrast() -> Self {
nord_theme(NordFlavor {
dark: false,
bg0: rgb(236, 239, 244), bg1: rgb(229, 233, 240), bg2: rgb(216, 222, 233), bg3: rgb(67, 76, 94), fg0: rgb(46, 52, 64), fg1: rgb(59, 66, 82), frost0: rgb(143, 188, 187), frost1: rgb(136, 192, 208), frost2: rgb(129, 161, 193), red: rgb(191, 97, 106), orange: rgb(208, 135, 112), yellow: rgb(235, 203, 139), green: rgb(163, 190, 140), syntect_theme: EmbeddedThemeName::Base16OceanLight,
})
}
}
#[derive(Clone, Copy)]
struct CatppuccinFlavor {
dark: bool,
text: Color,
subtext1: Color,
overlay1: Color,
overlay0: Color,
surface2: Color,
surface1: Color,
base: Color,
mantle: Color,
crust: Color,
red: Color,
yellow: Color,
green: Color,
teal: Color,
blue: Color,
lavender: Color,
peach: Color,
pink: Color,
}
#[derive(Clone, Copy)]
struct NordFlavor {
dark: bool,
bg0: Color,
bg1: Color,
bg2: Color,
bg3: Color,
fg0: Color,
fg1: Color,
frost0: Color,
frost1: Color,
frost2: Color,
red: Color,
orange: Color,
yellow: Color,
green: Color,
syntect_theme: EmbeddedThemeName,
}
#[derive(Clone, Copy)]
struct GruvboxFlavor {
dark: bool,
bg0: Color,
bg1: Color,
bg4: Color,
selected_bg: Color,
fg0: Color,
fg1: Color,
grey0: Color,
grey1: Color,
red: Color,
orange: Color,
yellow: Color,
green: Color,
aqua: Color,
blue: Color,
purple: Color,
bg_red: Color,
bg_green: Color,
}
fn rgb(r: u8, g: u8, b: u8) -> Color {
Color::Rgb(r, g, b)
}
fn blend(base: Color, accent: Color, accent_percent: u8) -> Color {
debug_assert!(accent_percent <= 100);
match (base, accent) {
(Color::Rgb(br, bg, bb), Color::Rgb(ar, ag, ab)) => {
let p = u16::from(accent_percent);
let inv = 100_u16.saturating_sub(p);
let mix =
|b: u8, a: u8| -> u8 { ((u16::from(b) * inv + u16::from(a) * p) / 100) as u8 };
rgb(mix(br, ar), mix(bg, ag), mix(bb, ab))
}
_ => accent,
}
}
fn catppuccin_theme(flavor: CatppuccinFlavor, syntect_theme: EmbeddedThemeName) -> Theme {
let accent_fg = if flavor.dark {
flavor.base
} else {
flavor.crust
};
let diff_add_bg = blend(flavor.base, flavor.green, 20);
let diff_del_bg = blend(flavor.base, flavor.red, 20);
let syntax_add_bg = blend(flavor.base, flavor.green, 16);
let syntax_del_bg = blend(flavor.base, flavor.red, 16);
Theme {
highlighter: OnceLock::new(),
panel_bg: flavor.base,
bg_highlight: flavor.surface1,
fg_primary: flavor.text,
fg_secondary: flavor.subtext1,
fg_dim: flavor.overlay0,
diff_add: flavor.green,
diff_add_bg,
diff_del: flavor.red,
diff_del_bg,
diff_context: flavor.text,
diff_hunk_header: flavor.blue,
expanded_context_fg: flavor.overlay1,
syntax_add_bg,
syntax_del_bg,
syntect_theme,
file_added: flavor.green,
file_modified: flavor.yellow,
file_deleted: flavor.red,
file_renamed: flavor.pink,
reviewed: flavor.green,
pending: flavor.yellow,
comment_note: flavor.blue,
comment_suggestion: flavor.teal,
comment_issue: flavor.red,
comment_praise: flavor.green,
comment_question: flavor.yellow,
border_focused: flavor.blue,
border_unfocused: flavor.surface2,
status_bar_bg: flavor.mantle,
cursor_color: flavor.peach,
branch_name: flavor.teal,
help_indicator: flavor.overlay0,
message_info_fg: accent_fg,
message_info_bg: flavor.teal,
message_warning_fg: accent_fg,
message_warning_bg: flavor.yellow,
message_error_fg: accent_fg,
message_error_bg: flavor.red,
update_badge_fg: accent_fg,
update_badge_bg: flavor.peach,
mode_fg: accent_fg,
mode_bg: flavor.lavender,
markdown_header: flavor.blue,
markdown_code: flavor.peach,
markdown_code_bg: flavor.mantle,
markdown_link: flavor.blue,
markdown_blockquote: flavor.overlay1,
markdown_rule: flavor.overlay0,
file_ref: flavor.teal,
risk_low: flavor.green,
risk_medium: flavor.yellow,
risk_high: flavor.red,
}
}
fn gruvbox_theme(flavor: GruvboxFlavor) -> Theme {
let syntect_theme = if flavor.dark {
EmbeddedThemeName::GruvboxDark
} else {
EmbeddedThemeName::GruvboxLight
};
let accent_fg = if flavor.dark { flavor.bg0 } else { flavor.fg1 };
Theme {
highlighter: OnceLock::new(),
panel_bg: flavor.bg0,
bg_highlight: flavor.selected_bg,
fg_primary: flavor.fg0,
fg_secondary: flavor.fg1,
fg_dim: flavor.grey0,
diff_add: flavor.green,
diff_add_bg: flavor.bg_green,
diff_del: flavor.red,
diff_del_bg: flavor.bg_red,
diff_context: flavor.fg0,
diff_hunk_header: flavor.blue,
expanded_context_fg: flavor.grey1,
syntax_add_bg: flavor.bg_green,
syntax_del_bg: flavor.bg_red,
syntect_theme,
file_added: flavor.green,
file_modified: flavor.yellow,
file_deleted: flavor.red,
file_renamed: flavor.purple,
reviewed: flavor.green,
pending: flavor.yellow,
comment_note: flavor.blue,
comment_suggestion: flavor.aqua,
comment_issue: flavor.red,
comment_praise: flavor.green,
comment_question: flavor.yellow,
border_focused: flavor.aqua,
border_unfocused: flavor.bg4,
status_bar_bg: flavor.bg1,
cursor_color: flavor.orange,
branch_name: flavor.aqua,
help_indicator: flavor.grey0,
message_info_fg: accent_fg,
message_info_bg: flavor.aqua,
message_warning_fg: accent_fg,
message_warning_bg: flavor.yellow,
message_error_fg: accent_fg,
message_error_bg: flavor.red,
update_badge_fg: accent_fg,
update_badge_bg: flavor.orange,
mode_fg: accent_fg,
mode_bg: flavor.green,
markdown_header: flavor.blue,
markdown_code: flavor.yellow,
markdown_code_bg: flavor.bg1,
markdown_link: flavor.aqua,
markdown_blockquote: flavor.grey0,
markdown_rule: flavor.grey0,
file_ref: flavor.aqua,
risk_low: flavor.green,
risk_medium: flavor.yellow,
risk_high: flavor.red,
}
}
fn nord_theme(flavor: NordFlavor) -> Theme {
let accent_fg = if flavor.dark { flavor.bg0 } else { flavor.fg1 };
let diff_add_bg = blend(flavor.bg0, flavor.green, 15);
let diff_del_bg = blend(flavor.bg0, flavor.red, 15);
let syntax_add_bg = blend(flavor.bg0, flavor.green, 10);
let syntax_del_bg = blend(flavor.bg0, flavor.red, 10);
Theme {
highlighter: OnceLock::new(),
panel_bg: flavor.bg0,
bg_highlight: flavor.bg1,
fg_primary: flavor.fg0,
fg_secondary: flavor.fg1,
fg_dim: flavor.bg3,
diff_add: flavor.green,
diff_add_bg,
diff_del: flavor.red,
diff_del_bg,
diff_context: flavor.fg0,
diff_hunk_header: flavor.frost1,
expanded_context_fg: flavor.bg3,
syntax_add_bg,
syntax_del_bg,
syntect_theme: flavor.syntect_theme,
file_added: flavor.green,
file_modified: flavor.yellow,
file_deleted: flavor.red,
file_renamed: flavor.frost2,
reviewed: flavor.green,
pending: flavor.yellow,
comment_note: flavor.frost1,
comment_suggestion: flavor.frost0,
comment_issue: flavor.red,
comment_praise: flavor.green,
comment_question: flavor.yellow,
border_focused: flavor.frost1,
border_unfocused: flavor.bg1,
status_bar_bg: flavor.bg2,
cursor_color: flavor.frost2,
branch_name: flavor.frost0,
help_indicator: flavor.bg3,
message_info_fg: accent_fg,
message_info_bg: flavor.frost1,
message_warning_fg: accent_fg,
message_warning_bg: flavor.orange,
message_error_fg: accent_fg,
message_error_bg: flavor.red,
update_badge_fg: accent_fg,
update_badge_bg: flavor.orange,
mode_fg: accent_fg,
mode_bg: flavor.frost1,
markdown_header: flavor.frost1,
markdown_code: flavor.yellow,
markdown_code_bg: flavor.bg1,
markdown_link: flavor.frost1,
markdown_blockquote: flavor.bg3,
markdown_rule: flavor.bg3,
file_ref: flavor.frost0,
risk_low: flavor.green,
risk_medium: flavor.yellow,
risk_high: flavor.red,
}
}
impl Theme {
pub fn gutter_bg(&self) -> Color {
let Color::Rgb(r, g, b) = self.panel_bg else {
return self.panel_bg;
};
let lum = u32::from(r) * 299 + u32::from(g) * 587 + u32::from(b) * 114;
let shift: i16 = if lum < 128_000 { 12 } else { -12 };
let adjust = |c: u8| -> u8 {
let v = i16::from(c) + shift;
v.clamp(0, 255) as u8
};
Color::Rgb(adjust(r), adjust(g), adjust(b))
}
pub fn syntax_highlighter(&self) -> &SyntaxHighlighter {
self.highlighter.get_or_init(|| {
SyntaxHighlighter::new(
self.syntect_theme,
ratatui_color_to_hint(self.syntax_add_bg),
ratatui_color_to_hint(self.syntax_del_bg),
)
})
}
}
fn ratatui_color_to_hint(color: ratatui::style::Color) -> travelagent_core::style::ColorHint {
if let ratatui::style::Color::Rgb(r, g, b) = color {
travelagent_core::style::ColorHint::rgb(r, g, b)
} else {
travelagent_core::style::ColorHint::rgb(0, 0, 0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn should_use_dark_bg_for_nord_dark_mode_foreground() {
let theme = Theme::nord_dark();
assert_eq!(theme.mode_fg, Color::Rgb(46, 52, 64)); }
#[test]
fn should_use_fg1_for_nord_light_mode_foreground() {
let theme = Theme::nord_light();
assert_eq!(theme.mode_fg, Color::Rgb(59, 66, 82)); }
#[test]
fn should_use_dark_flavor_base_for_catppuccin_mode_foreground() {
let theme = Theme::catppuccin_mocha();
assert_eq!(theme.mode_fg, Color::Rgb(30, 30, 46));
}
#[test]
fn should_use_light_flavor_crust_for_catppuccin_mode_foreground() {
let theme = Theme::catppuccin_latte();
assert_eq!(theme.mode_fg, Color::Rgb(220, 224, 232));
}
#[test]
fn should_blend_to_base_at_zero_percent() {
let base = Color::Rgb(10, 20, 30);
let accent = Color::Rgb(200, 210, 220);
assert_eq!(blend(base, accent, 0), base);
}
#[test]
fn should_blend_to_accent_at_hundred_percent() {
let base = Color::Rgb(10, 20, 30);
let accent = Color::Rgb(200, 210, 220);
assert_eq!(blend(base, accent, 100), accent);
}
#[test]
fn should_blend_midpoint_with_integer_rounding() {
let base = Color::Rgb(0, 10, 20);
let accent = Color::Rgb(100, 110, 120);
assert_eq!(blend(base, accent, 50), Color::Rgb(50, 60, 70));
}
#[test]
fn should_return_accent_for_non_rgb_blend_inputs() {
let accent = Color::Rgb(100, 110, 120);
assert_eq!(blend(Color::Reset, accent, 50), accent);
}
#[test]
fn every_theme_has_three_distinct_risk_colors() {
for (name, theme) in [
("dark", Theme::dark()),
("light", Theme::light()),
("solarized_light", Theme::solarized_light()),
("solarized_dark", Theme::solarized_dark()),
("ayu_light", Theme::ayu_light()),
("onedark", Theme::onedark()),
("catppuccin_latte", Theme::catppuccin_latte()),
("catppuccin_frappe", Theme::catppuccin_frappe()),
("catppuccin_macchiato", Theme::catppuccin_macchiato()),
("catppuccin_mocha", Theme::catppuccin_mocha()),
("gruvbox_dark", Theme::gruvbox_dark()),
("gruvbox_light", Theme::gruvbox_light()),
("nord_dark", Theme::nord_dark()),
("nord_light", Theme::nord_light()),
("nord_dark_high_contrast", Theme::nord_dark_high_contrast()),
(
"nord_light_high_contrast",
Theme::nord_light_high_contrast(),
),
] {
assert_ne!(
theme.risk_low, theme.risk_medium,
"{name}: risk_low should differ from risk_medium"
);
assert_ne!(
theme.risk_medium, theme.risk_high,
"{name}: risk_medium should differ from risk_high"
);
assert_ne!(
theme.risk_low, theme.risk_high,
"{name}: risk_low should differ from risk_high"
);
}
}
}