Skip to content

Commit d60bd2e

Browse files
committed
feat(theme): add customizable theme system with live switching
Add comprehensive theme support for the TUI with real-time theme switching: Core: - Add ThemeConfig struct with serde support for persistence - Define 4 built-in themes: dark, light, ocean_dark, monokai - Add ThemeColors with from_name() lookup and available_themes() list - Add MarkdownTheme variants for each theme with full color customization TUI: - Add ThemeSelectorModal for interactive theme selection - Add 'Change Theme' command to command palette (Settings category) - Extend AdaptiveColors with theme support methods - Add theme state to AppState with set_theme() method Integration: - Update rendering pipeline to use theme from state - Add themed markdown rendering for conversation display - Persist last selected theme to config file - Load saved theme preference on app startup
1 parent 30b18f9 commit d60bd2e

File tree

18 files changed

+1155
-30
lines changed

18 files changed

+1155
-30
lines changed

src/cortex-core/src/markdown/theme.rs

Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,209 @@ impl MarkdownTheme {
380380
}
381381
}
382382

383+
impl MarkdownTheme {
384+
/// Create a markdown theme for the "dark" theme (same as default)
385+
pub fn dark() -> Self {
386+
Self::default()
387+
}
388+
389+
/// Create a markdown theme for the "light" theme
390+
pub fn light() -> Self {
391+
Self {
392+
h1: Style::default()
393+
.fg(Color::Rgb(0, 100, 70))
394+
.add_modifier(Modifier::BOLD | Modifier::UNDERLINED),
395+
h2: Style::default()
396+
.fg(Color::Rgb(0, 100, 70))
397+
.add_modifier(Modifier::BOLD),
398+
h3: Style::default()
399+
.fg(Color::Rgb(0, 80, 60))
400+
.add_modifier(Modifier::BOLD | Modifier::ITALIC),
401+
h4: Style::default()
402+
.fg(Color::Rgb(0, 80, 60))
403+
.add_modifier(Modifier::ITALIC),
404+
h5: Style::default()
405+
.fg(Color::Rgb(80, 80, 80))
406+
.add_modifier(Modifier::ITALIC),
407+
h6: Style::default()
408+
.fg(Color::Rgb(120, 120, 120))
409+
.add_modifier(Modifier::ITALIC),
410+
bold: Style::default()
411+
.fg(Color::Rgb(0, 100, 70))
412+
.add_modifier(Modifier::BOLD),
413+
italic: Style::default()
414+
.fg(Color::Rgb(30, 30, 30))
415+
.add_modifier(Modifier::ITALIC),
416+
strikethrough: Style::default()
417+
.fg(Color::Rgb(100, 100, 100))
418+
.add_modifier(Modifier::CROSSED_OUT),
419+
code_inline: Style::default()
420+
.fg(Color::Rgb(0, 80, 60))
421+
.bg(Color::Rgb(235, 235, 235)),
422+
code_block_bg: Color::Rgb(245, 245, 245),
423+
code_block_border: Color::Rgb(200, 200, 200),
424+
code_block_text: Style::default().fg(Color::Rgb(30, 30, 30)),
425+
code_lang_tag: Style::default()
426+
.fg(Color::Rgb(50, 100, 200))
427+
.add_modifier(Modifier::ITALIC),
428+
blockquote_border: Color::Rgb(0, 100, 70),
429+
blockquote_text: Style::default()
430+
.fg(Color::Rgb(80, 80, 80))
431+
.add_modifier(Modifier::ITALIC),
432+
list_bullet: Style::default().fg(Color::Rgb(0, 100, 70)),
433+
list_number: Style::default().fg(Color::Rgb(0, 100, 70)),
434+
task_checked: Style::default().fg(Color::Rgb(0, 150, 0)),
435+
task_unchecked: Style::default().fg(Color::Rgb(120, 120, 120)),
436+
table_border: Color::Rgb(0, 100, 70),
437+
table_header_bg: Color::Rgb(235, 235, 235),
438+
table_header_text: Style::default()
439+
.fg(Color::Rgb(30, 30, 30))
440+
.add_modifier(Modifier::BOLD),
441+
table_cell_text: Style::default().fg(Color::Rgb(30, 30, 30)),
442+
link_text: Style::default()
443+
.fg(Color::Rgb(50, 100, 200))
444+
.add_modifier(Modifier::UNDERLINED),
445+
link_url: Style::default().fg(Color::Rgb(120, 120, 120)),
446+
hr: Style::default().fg(Color::Rgb(200, 200, 200)),
447+
text: Style::default().fg(Color::Rgb(30, 30, 30)),
448+
}
449+
}
450+
451+
/// Create a markdown theme for the "ocean_dark" theme
452+
pub fn ocean_dark() -> Self {
453+
Self {
454+
h1: Style::default()
455+
.fg(Color::Rgb(0, 200, 200))
456+
.add_modifier(Modifier::BOLD | Modifier::UNDERLINED),
457+
h2: Style::default()
458+
.fg(Color::Rgb(0, 200, 200))
459+
.add_modifier(Modifier::BOLD),
460+
h3: Style::default()
461+
.fg(Color::Rgb(100, 200, 220))
462+
.add_modifier(Modifier::BOLD | Modifier::ITALIC),
463+
h4: Style::default()
464+
.fg(Color::Rgb(100, 200, 220))
465+
.add_modifier(Modifier::ITALIC),
466+
h5: Style::default()
467+
.fg(Color::Rgb(140, 170, 200))
468+
.add_modifier(Modifier::ITALIC),
469+
h6: Style::default()
470+
.fg(Color::Rgb(80, 110, 140))
471+
.add_modifier(Modifier::ITALIC),
472+
bold: Style::default()
473+
.fg(Color::Rgb(0, 200, 200))
474+
.add_modifier(Modifier::BOLD),
475+
italic: Style::default()
476+
.fg(Color::Rgb(230, 240, 250))
477+
.add_modifier(Modifier::ITALIC),
478+
strikethrough: Style::default()
479+
.fg(Color::Rgb(140, 170, 200))
480+
.add_modifier(Modifier::CROSSED_OUT),
481+
code_inline: Style::default()
482+
.fg(Color::Rgb(100, 180, 255))
483+
.bg(Color::Rgb(25, 50, 80)),
484+
code_block_bg: Color::Rgb(15, 35, 60),
485+
code_block_border: Color::Rgb(40, 80, 120),
486+
code_block_text: Style::default().fg(Color::Rgb(230, 240, 250)),
487+
code_lang_tag: Style::default()
488+
.fg(Color::Rgb(100, 180, 255))
489+
.add_modifier(Modifier::ITALIC),
490+
blockquote_border: Color::Rgb(0, 180, 180),
491+
blockquote_text: Style::default()
492+
.fg(Color::Rgb(140, 170, 200))
493+
.add_modifier(Modifier::ITALIC),
494+
list_bullet: Style::default().fg(Color::Rgb(0, 200, 200)),
495+
list_number: Style::default().fg(Color::Rgb(0, 200, 200)),
496+
task_checked: Style::default().fg(Color::Rgb(0, 220, 180)),
497+
task_unchecked: Style::default().fg(Color::Rgb(80, 110, 140)),
498+
table_border: Color::Rgb(0, 200, 200),
499+
table_header_bg: Color::Rgb(25, 50, 80),
500+
table_header_text: Style::default()
501+
.fg(Color::Rgb(230, 240, 250))
502+
.add_modifier(Modifier::BOLD),
503+
table_cell_text: Style::default().fg(Color::Rgb(230, 240, 250)),
504+
link_text: Style::default()
505+
.fg(Color::Rgb(100, 180, 255))
506+
.add_modifier(Modifier::UNDERLINED),
507+
link_url: Style::default().fg(Color::Rgb(80, 110, 140)),
508+
hr: Style::default().fg(Color::Rgb(40, 80, 120)),
509+
text: Style::default().fg(Color::Rgb(230, 240, 250)),
510+
}
511+
}
512+
513+
/// Create a markdown theme for the "monokai" theme
514+
pub fn monokai() -> Self {
515+
Self {
516+
h1: Style::default()
517+
.fg(Color::Rgb(166, 226, 46)) // Monokai green
518+
.add_modifier(Modifier::BOLD | Modifier::UNDERLINED),
519+
h2: Style::default()
520+
.fg(Color::Rgb(166, 226, 46))
521+
.add_modifier(Modifier::BOLD),
522+
h3: Style::default()
523+
.fg(Color::Rgb(102, 217, 239)) // Monokai cyan
524+
.add_modifier(Modifier::BOLD | Modifier::ITALIC),
525+
h4: Style::default()
526+
.fg(Color::Rgb(102, 217, 239))
527+
.add_modifier(Modifier::ITALIC),
528+
h5: Style::default()
529+
.fg(Color::Rgb(180, 180, 170))
530+
.add_modifier(Modifier::ITALIC),
531+
h6: Style::default()
532+
.fg(Color::Rgb(117, 113, 94)) // Monokai comment
533+
.add_modifier(Modifier::ITALIC),
534+
bold: Style::default()
535+
.fg(Color::Rgb(249, 38, 114)) // Monokai pink
536+
.add_modifier(Modifier::BOLD),
537+
italic: Style::default()
538+
.fg(Color::Rgb(248, 248, 242)) // Monokai white
539+
.add_modifier(Modifier::ITALIC),
540+
strikethrough: Style::default()
541+
.fg(Color::Rgb(117, 113, 94))
542+
.add_modifier(Modifier::CROSSED_OUT),
543+
code_inline: Style::default()
544+
.fg(Color::Rgb(230, 219, 116)) // Monokai yellow
545+
.bg(Color::Rgb(55, 56, 50)),
546+
code_block_bg: Color::Rgb(45, 46, 40),
547+
code_block_border: Color::Rgb(70, 71, 65),
548+
code_block_text: Style::default().fg(Color::Rgb(248, 248, 242)),
549+
code_lang_tag: Style::default()
550+
.fg(Color::Rgb(102, 217, 239))
551+
.add_modifier(Modifier::ITALIC),
552+
blockquote_border: Color::Rgb(166, 226, 46),
553+
blockquote_text: Style::default()
554+
.fg(Color::Rgb(117, 113, 94))
555+
.add_modifier(Modifier::ITALIC),
556+
list_bullet: Style::default().fg(Color::Rgb(249, 38, 114)),
557+
list_number: Style::default().fg(Color::Rgb(249, 38, 114)),
558+
task_checked: Style::default().fg(Color::Rgb(166, 226, 46)),
559+
task_unchecked: Style::default().fg(Color::Rgb(117, 113, 94)),
560+
table_border: Color::Rgb(166, 226, 46),
561+
table_header_bg: Color::Rgb(55, 56, 50),
562+
table_header_text: Style::default()
563+
.fg(Color::Rgb(248, 248, 242))
564+
.add_modifier(Modifier::BOLD),
565+
table_cell_text: Style::default().fg(Color::Rgb(248, 248, 242)),
566+
link_text: Style::default()
567+
.fg(Color::Rgb(102, 217, 239))
568+
.add_modifier(Modifier::UNDERLINED),
569+
link_url: Style::default().fg(Color::Rgb(117, 113, 94)),
570+
hr: Style::default().fg(Color::Rgb(70, 71, 65)),
571+
text: Style::default().fg(Color::Rgb(248, 248, 242)),
572+
}
573+
}
574+
575+
/// Create a markdown theme from a theme name
576+
pub fn from_name(name: &str) -> Self {
577+
match name.to_lowercase().as_str() {
578+
"light" => Self::light(),
579+
"ocean_dark" | "ocean" => Self::ocean_dark(),
580+
"monokai" => Self::monokai(),
581+
"dark" | _ => Self::dark(),
582+
}
583+
}
584+
}
585+
383586
impl Default for MarkdownTheme {
384587
fn default() -> Self {
385588
Self {
@@ -902,4 +1105,105 @@ mod tests {
9021105
assert!(theme.hr.fg.is_some());
9031106
assert!(theme.text.fg.is_some());
9041107
}
1108+
1109+
#[test]
1110+
fn test_dark_theme() {
1111+
let theme = MarkdownTheme::dark();
1112+
let default_theme = MarkdownTheme::default();
1113+
1114+
// dark() should return the same as default()
1115+
assert_eq!(theme.h1.fg, default_theme.h1.fg);
1116+
assert_eq!(theme.bold.fg, default_theme.bold.fg);
1117+
assert_eq!(theme.code_block_bg, default_theme.code_block_bg);
1118+
}
1119+
1120+
#[test]
1121+
fn test_light_theme() {
1122+
let theme = MarkdownTheme::light();
1123+
1124+
// Light theme has different colors than dark
1125+
assert_eq!(theme.h1.fg, Some(Color::Rgb(0, 100, 70)));
1126+
assert_eq!(theme.text.fg, Some(Color::Rgb(30, 30, 30)));
1127+
assert_eq!(theme.code_block_bg, Color::Rgb(245, 245, 245));
1128+
}
1129+
1130+
#[test]
1131+
fn test_ocean_dark_theme() {
1132+
let theme = MarkdownTheme::ocean_dark();
1133+
1134+
// Ocean dark theme has cyan accent
1135+
assert_eq!(theme.h1.fg, Some(Color::Rgb(0, 200, 200)));
1136+
assert_eq!(theme.text.fg, Some(Color::Rgb(230, 240, 250)));
1137+
assert_eq!(theme.code_block_bg, Color::Rgb(15, 35, 60));
1138+
}
1139+
1140+
#[test]
1141+
fn test_monokai_theme() {
1142+
let theme = MarkdownTheme::monokai();
1143+
1144+
// Monokai theme has distinctive green headers
1145+
assert_eq!(theme.h1.fg, Some(Color::Rgb(166, 226, 46)));
1146+
assert_eq!(theme.bold.fg, Some(Color::Rgb(249, 38, 114))); // Monokai pink
1147+
assert_eq!(theme.code_block_bg, Color::Rgb(45, 46, 40));
1148+
}
1149+
1150+
#[test]
1151+
fn test_from_name() {
1152+
// Test all valid theme names
1153+
let dark = MarkdownTheme::from_name("dark");
1154+
assert_eq!(dark.h1.fg, MarkdownTheme::dark().h1.fg);
1155+
1156+
let light = MarkdownTheme::from_name("light");
1157+
assert_eq!(light.h1.fg, MarkdownTheme::light().h1.fg);
1158+
1159+
let ocean = MarkdownTheme::from_name("ocean_dark");
1160+
assert_eq!(ocean.h1.fg, MarkdownTheme::ocean_dark().h1.fg);
1161+
1162+
let ocean_short = MarkdownTheme::from_name("ocean");
1163+
assert_eq!(ocean_short.h1.fg, MarkdownTheme::ocean_dark().h1.fg);
1164+
1165+
let monokai = MarkdownTheme::from_name("monokai");
1166+
assert_eq!(monokai.h1.fg, MarkdownTheme::monokai().h1.fg);
1167+
}
1168+
1169+
#[test]
1170+
fn test_from_name_case_insensitive() {
1171+
// Should be case insensitive
1172+
let dark_upper = MarkdownTheme::from_name("DARK");
1173+
let dark_lower = MarkdownTheme::from_name("dark");
1174+
let dark_mixed = MarkdownTheme::from_name("DaRk");
1175+
1176+
assert_eq!(dark_upper.h1.fg, dark_lower.h1.fg);
1177+
assert_eq!(dark_mixed.h1.fg, dark_lower.h1.fg);
1178+
}
1179+
1180+
#[test]
1181+
fn test_from_name_fallback() {
1182+
// Unknown names should fall back to dark
1183+
let unknown = MarkdownTheme::from_name("unknown");
1184+
let dark = MarkdownTheme::dark();
1185+
1186+
assert_eq!(unknown.h1.fg, dark.h1.fg);
1187+
}
1188+
1189+
#[test]
1190+
fn test_all_theme_colors_are_set() {
1191+
// Verify all theme variants have colors set
1192+
for theme in [
1193+
MarkdownTheme::dark(),
1194+
MarkdownTheme::light(),
1195+
MarkdownTheme::ocean_dark(),
1196+
MarkdownTheme::monokai(),
1197+
] {
1198+
assert!(theme.h1.fg.is_some());
1199+
assert!(theme.h2.fg.is_some());
1200+
assert!(theme.h3.fg.is_some());
1201+
assert!(theme.h4.fg.is_some());
1202+
assert!(theme.h5.fg.is_some());
1203+
assert!(theme.h6.fg.is_some());
1204+
assert!(theme.bold.fg.is_some());
1205+
assert!(theme.italic.fg.is_some());
1206+
assert!(theme.text.fg.is_some());
1207+
}
1208+
}
9051209
}

0 commit comments

Comments
 (0)