Switch streaming markdown formatter to Catppuccin Macchiato color scheme
Replace Dracula-era hardcoded ANSI colors with named constants from the Catppuccin Macchiato palette. All semantic roles now use 24-bit RGB values: Headers: Mauve (H1), Blue (H2), Lavender (H3), Teal (H4), Subtext1 (H5+) Bold: Sky (#91d7e3) Italic: Sapphire (#7dc4e4) Inline code: Peach (#f5a97f) Links: Green (#a6da95) underlined HR/labels: Overlay1 (#8087a2) Also switches syntect code highlighting theme from base16-ocean.dark to base16-mocha.dark for better palette consistency.
This commit is contained in:
@@ -22,6 +22,35 @@ use termimad::MadSkin;
|
||||
static SYNTAX_SET: Lazy<SyntaxSet> = Lazy::new(SyntaxSet::load_defaults_newlines);
|
||||
static THEME_SET: Lazy<ThemeSet> = Lazy::new(ThemeSet::load_defaults);
|
||||
|
||||
// ── Catppuccin Macchiato palette ──────────────────────────────────────────────
|
||||
// https://github.com/catppuccin/catppuccin (Macchiato variant)
|
||||
//
|
||||
// Each constant is an ANSI 24-bit color prefix: \x1b[38;2;R;G;Bm
|
||||
// Combine with style modifiers (1=bold, 3=italic, 4=underline, 9=strike) as needed.
|
||||
|
||||
/// Mauve #c6a0f6 — H1 headers
|
||||
const MAUVE: &str = "\x1b[38;2;198;160;246m";
|
||||
/// Blue #8aadf4 — H2 headers
|
||||
const BLUE: &str = "\x1b[38;2;138;173;244m";
|
||||
/// Lavender #b7bdf8 — H3 headers
|
||||
const LAVENDER: &str = "\x1b[38;2;183;189;248m";
|
||||
/// Teal #8bd5ca — H4 headers
|
||||
const TEAL: &str = "\x1b[38;2;139;213;202m";
|
||||
/// Subtext1 #a5adcb — H5/H6 headers (dim)
|
||||
const SUBTEXT1: &str = "\x1b[38;2;165;173;203m";
|
||||
/// Sky #91d7e3 — bold text
|
||||
const SKY: &str = "\x1b[38;2;145;215;227m";
|
||||
/// Sapphire #7dc4e4 — italic text
|
||||
const SAPPHIRE: &str = "\x1b[38;2;125;196;228m";
|
||||
/// Peach #f5a97f — inline code
|
||||
const PEACH: &str = "\x1b[38;2;245;169;127m";
|
||||
/// Green #a6da95 — links
|
||||
const GREEN: &str = "\x1b[38;2;166;218;149m";
|
||||
/// Overlay1 #8087a2 — horizontal rules, muted elements
|
||||
const OVERLAY1: &str = "\x1b[38;2;128;135;162m";
|
||||
/// Reset all attributes
|
||||
const RESET: &str = "\x1b[0m";
|
||||
|
||||
/// Types of markdown delimiters we track.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum DelimiterKind {
|
||||
@@ -528,7 +557,7 @@ impl StreamingMarkdownFormatter {
|
||||
|| (trimmed.len() >= 3 && trimmed.chars().all(|c| c == '_'));
|
||||
if is_hr {
|
||||
// Emit a horizontal rule
|
||||
self.pending_output.push_back("\x1b[2m────────────────────────────────────────\x1b[0m\n".to_string());
|
||||
self.pending_output.push_back(format!("{}────────────────────────────────────────{}\n", OVERLAY1, RESET));
|
||||
self.current_line.clear();
|
||||
self.delimiter_stack.clear();
|
||||
return;
|
||||
@@ -569,12 +598,12 @@ impl StreamingMarkdownFormatter {
|
||||
// Format based on level (magenta, bold for h1/h2)
|
||||
// We wrap the already-formatted content in header color, then reset at the end
|
||||
match level {
|
||||
1 => format!("\x1b[1;95m{}\x1b[0m\n", formatted_content), // Bold pink (Dracula)
|
||||
2 => format!("\x1b[35m{}\x1b[0m\n", formatted_content), // Purple/magenta (Dracula)
|
||||
3 => format!("\x1b[36m{}\x1b[0m\n", formatted_content), // Cyan (Dracula)
|
||||
4 => format!("\x1b[37m{}\x1b[0m\n", formatted_content), // White (Dracula)
|
||||
5 => format!("\x1b[2m{}\x1b[0m\n", formatted_content), // Dim (Dracula)
|
||||
_ => format!("\x1b[2m{}\x1b[0m\n", formatted_content), // Dim for h6+ (Dracula)
|
||||
1 => format!("\x1b[1m{}{}{}\n", MAUVE, formatted_content, RESET), // Bold Mauve
|
||||
2 => format!("{}{}{}\n", BLUE, formatted_content, RESET), // Blue
|
||||
3 => format!("{}{}{}\n", LAVENDER, formatted_content, RESET), // Lavender
|
||||
4 => format!("{}{}{}\n", TEAL, formatted_content, RESET), // Teal
|
||||
5 => format!("\x1b[2m{}{}{}\n", SUBTEXT1, formatted_content, RESET), // Dim Subtext1
|
||||
_ => format!("\x1b[2m{}{}{}\n", SUBTEXT1, formatted_content, RESET), // Dim Subtext1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -607,14 +636,14 @@ impl StreamingMarkdownFormatter {
|
||||
let text = &caps[1];
|
||||
// Format any inline code within the link text
|
||||
let formatted_text = format_inline_code_only(text);
|
||||
format!("\x1b[36;4m{}\x1b[0m", formatted_text)
|
||||
format!("\x1b[4m{}{}{}", GREEN, formatted_text, RESET)
|
||||
}).to_string();
|
||||
|
||||
// Process inline code `code` -> code (in orange)
|
||||
let code_re = regex::Regex::new(r"`([^`]+)`").unwrap();
|
||||
result = code_re.replace_all(&result, |caps: ®ex::Captures| {
|
||||
let code = &caps[1];
|
||||
format!("\x1b[38;2;216;177;114m{}\x1b[0m", code)
|
||||
format!("{}{}{}", PEACH, code, RESET)
|
||||
}).to_string();
|
||||
|
||||
// Handle unclosed inline code at end of line: `code without closing backtick
|
||||
@@ -622,7 +651,7 @@ impl StreamingMarkdownFormatter {
|
||||
let unclosed_code_re = regex::Regex::new(r"`([^`]+)$").unwrap();
|
||||
result = unclosed_code_re.replace_all(&result, |caps: ®ex::Captures| {
|
||||
let code = &caps[1];
|
||||
format!("\x1b[38;2;216;177;114m{}\x1b[0m", code)
|
||||
format!("{}{}{}", PEACH, code, RESET)
|
||||
}).to_string();
|
||||
|
||||
// Process strikethrough ~~text~~ -> text (with strikethrough)
|
||||
@@ -645,7 +674,7 @@ impl StreamingMarkdownFormatter {
|
||||
let text = &caps[1];
|
||||
// Process nested italic within bold
|
||||
let inner = format_nested_italic(text);
|
||||
format!("\x1b[1;32m{}\x1b[0m", inner)
|
||||
format!("\x1b[1m{}{}{}", SKY, inner, RESET)
|
||||
}).to_string();
|
||||
|
||||
// Restore escaped characters (remove the placeholder markers)
|
||||
@@ -668,7 +697,7 @@ impl StreamingMarkdownFormatter {
|
||||
// Emit language label
|
||||
if let Some(ref l) = lang {
|
||||
self.pending_output
|
||||
.push_back(format!("\x1b[2;3m{}\x1b[0m\n", l));
|
||||
.push_back(format!("\x1b[2;3m{}{}{}\n", OVERLAY1, l, RESET));
|
||||
}
|
||||
|
||||
// Highlight the code
|
||||
@@ -765,7 +794,7 @@ fn format_inline_code_only(text: &str) -> String {
|
||||
let code_re = regex::Regex::new(r"`([^`]+)`").unwrap();
|
||||
code_re.replace_all(text, |caps: ®ex::Captures| {
|
||||
let code = &caps[1];
|
||||
format!("\x1b[38;2;216;177;114m{}\x1b[0m", code)
|
||||
format!("{}{}{}", PEACH, code, RESET)
|
||||
}).to_string()
|
||||
}
|
||||
|
||||
@@ -774,7 +803,7 @@ fn format_nested_italic(text: &str) -> String {
|
||||
let italic_re = regex::Regex::new(r"\*([^*]+)\*").unwrap();
|
||||
italic_re.replace_all(text, |caps: ®ex::Captures| {
|
||||
let inner = &caps[1];
|
||||
format!("\x1b[3;36m{}\x1b[0m\x1b[1;32m", inner) // italic, then restore bold
|
||||
format!("\x1b[3m{}{}{}\x1b[1m{}", SAPPHIRE, inner, RESET, SKY) // italic sapphire, then restore bold sky
|
||||
}).to_string()
|
||||
}
|
||||
|
||||
@@ -783,7 +812,7 @@ fn format_nested_bold(text: &str) -> String {
|
||||
let bold_re = regex::Regex::new(r"\*\*(.+?)\*\*").unwrap();
|
||||
bold_re.replace_all(text, |caps: ®ex::Captures| {
|
||||
let inner = &caps[1];
|
||||
format!("\x1b[1;32m{}\x1b[0m\x1b[3;36m", inner) // bold, then restore italic
|
||||
format!("\x1b[1m{}{}{}\x1b[3m{}", SKY, inner, RESET, SAPPHIRE) // bold sky, then restore italic sapphire
|
||||
}).to_string()
|
||||
}
|
||||
|
||||
@@ -819,7 +848,7 @@ fn process_italic_with_nested_bold(text: &str) -> String {
|
||||
let inner: String = chars[start..end_pos].iter().collect();
|
||||
// Process nested bold within the italic content
|
||||
let formatted_inner = format_nested_bold(&inner);
|
||||
result.push_str(&format!("\x1b[3;36m{}\x1b[0m", formatted_inner));
|
||||
result.push_str(&format!("\x1b[3m{}{}{}", SAPPHIRE, formatted_inner, RESET));
|
||||
i = end_pos + 1;
|
||||
} else {
|
||||
// No closing *, just output the *
|
||||
@@ -860,7 +889,7 @@ fn highlight_code(code: &str, lang: Option<&str>) -> String {
|
||||
.and_then(|_| normalized_lang.and_then(|l| SYNTAX_SET.find_syntax_by_token(l)))
|
||||
.unwrap_or_else(|| SYNTAX_SET.find_syntax_plain_text());
|
||||
|
||||
let theme = &THEME_SET.themes["base16-ocean.dark"];
|
||||
let theme = &THEME_SET.themes["base16-mocha.dark"];
|
||||
let mut highlighter = HighlightLines::new(syntax, theme);
|
||||
|
||||
let mut output = String::new();
|
||||
|
||||
@@ -562,8 +562,8 @@ fn test_bold_formatting() {
|
||||
eprintln!("Input: {:?}", input);
|
||||
eprintln!("Output: {:?}", full_output);
|
||||
|
||||
// Should contain green bold ANSI code (\x1b[1;32m)
|
||||
assert!(full_output.contains("\x1b[1;32m"), "Should contain bold formatting");
|
||||
// Should contain sky bold ANSI code (Catppuccin Macchiato)
|
||||
assert!(full_output.contains("\x1b[1m\x1b[38;2;145;215;227m"), "Should contain bold formatting");
|
||||
// Should NOT contain raw **
|
||||
assert!(!full_output.contains("**"), "Should not contain raw **");
|
||||
}
|
||||
@@ -611,20 +611,20 @@ Normal text with **bold**, *italic*, and `inline code` all together.
|
||||
eprintln!("=== END ===");
|
||||
|
||||
// Check headers are formatted (Dracula colors)
|
||||
assert!(full_output.contains("\x1b[1;95mHeader 1"), "H1 should be bold pink");
|
||||
assert!(full_output.contains("\x1b[35mHeader 2"), "H2 should be magenta");
|
||||
assert!(full_output.contains("\x1b[1m\x1b[38;2;198;160;246mHeader 1"), "H1 should be bold mauve");
|
||||
assert!(full_output.contains("\x1b[38;2;138;173;244mHeader 2"), "H2 should be blue");
|
||||
|
||||
// Check bold is green
|
||||
assert!(full_output.contains("\x1b[1;32mbold text\x1b[0m"), "Bold should be green");
|
||||
assert!(full_output.contains("\x1b[1m\x1b[38;2;145;215;227mbold text\x1b[0m"), "Bold should be sky");
|
||||
|
||||
// Check italic is cyan
|
||||
assert!(full_output.contains("\x1b[3;36mitalic text\x1b[0m"), "Italic should be cyan");
|
||||
assert!(full_output.contains("\x1b[3m\x1b[38;2;125;196;228mitalic text\x1b[0m"), "Italic should be sapphire");
|
||||
|
||||
// Check inline code is orange
|
||||
assert!(full_output.contains("\x1b[38;2;216;177;114minline code\x1b[0m"), "Inline code should be orange");
|
||||
assert!(full_output.contains("\x1b[38;2;245;169;127minline code\x1b[0m"), "Inline code should be peach");
|
||||
|
||||
// Check link is cyan underlined
|
||||
assert!(full_output.contains("\x1b[36;4mlink\x1b[0m"), "Link should be cyan underlined");
|
||||
assert!(full_output.contains("\x1b[4m\x1b[38;2;166;218;149mlink\x1b[0m"), "Link should be green underlined");
|
||||
|
||||
// Check bullets
|
||||
assert!(full_output.contains("• Bullet item 1"), "Should have bullet");
|
||||
@@ -662,7 +662,7 @@ fn test_unclosed_inline_code() {
|
||||
assert!(!full_output.contains('`'), "Should not contain raw backtick");
|
||||
|
||||
// Should contain orange formatting for the unclosed code
|
||||
assert!(full_output.contains("\x1b[38;2;216;177;114m"), "Should have orange formatting");
|
||||
assert!(full_output.contains("\x1b[38;2;245;169;127m"), "Should have peach formatting");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -705,11 +705,11 @@ Your config already has it set up with consult:
|
||||
|
||||
// Headers should be formatted (H3 = cyan in Dracula), not raw
|
||||
assert!(!full_output.contains("### Key"), "Should not have raw ### header");
|
||||
assert!(full_output.contains("\x1b[36mKey bindings"), "H3 header should be cyan");
|
||||
assert!(full_output.contains("\x1b[38;2;183;189;248mKey bindings"), "H3 header should be lavender");
|
||||
|
||||
// Bold should be formatted, not raw
|
||||
assert!(!full_output.contains("**C-x p f**"), "Should not have raw ** bold");
|
||||
assert!(full_output.contains("\x1b[1;32mC-x p f\x1b[0m"), "Bold should be green");
|
||||
assert!(full_output.contains("\x1b[1m\x1b[38;2;145;215;227mC-x p f\x1b[0m"), "Bold should be sky");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -823,7 +823,7 @@ Some **bold** text.
|
||||
|
||||
// Header should be formatted (H3 = cyan in Dracula)
|
||||
assert!(!full_output.contains("### Header"), "Should not have raw ### header");
|
||||
assert!(full_output.contains("\x1b[36mHeader after table"), "H3 header should be cyan");
|
||||
assert!(full_output.contains("\x1b[38;2;183;189;248mHeader after table"), "H3 header should be lavender");
|
||||
|
||||
// Bold should be formatted
|
||||
assert!(!full_output.contains("**bold**"), "Should not have raw ** bold");
|
||||
@@ -1011,7 +1011,7 @@ fn test_simple_italic() {
|
||||
let mut fmt = make_formatter();
|
||||
let out = fmt.process("*simple italic*\n");
|
||||
eprintln!("Simple italic: {:?}", out);
|
||||
assert!(out.contains("\x1b[3;36m"), "Should have italic formatting");
|
||||
assert!(out.contains("\x1b[3m\x1b[38;2;125;196;228m"), "Should have italic formatting");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1020,9 +1020,9 @@ fn test_italic_with_nested_bold() {
|
||||
let output = fmt.process("*italic with **nested bold** inside*\n");
|
||||
eprintln!("Output: {:?}", output);
|
||||
// Should have italic formatting (cyan)
|
||||
assert!(output.contains("\x1b[3;36m"), "Should have italic formatting");
|
||||
assert!(output.contains("\x1b[3m\x1b[38;2;125;196;228m"), "Should have italic formatting");
|
||||
// Should have bold formatting (green) for nested bold
|
||||
assert!(output.contains("\x1b[1;32m"), "Should have bold formatting for nested");
|
||||
assert!(output.contains("\x1b[1m\x1b[38;2;145;215;227m"), "Should have bold formatting for nested");
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
@@ -1790,10 +1790,10 @@ fn test_bold_inside_header() {
|
||||
assert!(!full.contains("**"), "Should not contain raw ** markers, got: {}", full);
|
||||
|
||||
// Should have header formatting (H1 = bold pink in Dracula)
|
||||
assert!(full.contains("\x1b[1;95m"), "Should have bold pink header formatting");
|
||||
assert!(full.contains("\x1b[1m\x1b[38;2;198;160;246m"), "Should have bold mauve header formatting");
|
||||
|
||||
// Should have bold formatting (green) for the bold text inside
|
||||
assert!(full.contains("\x1b[1;32m"), "Should have green bold formatting for **Bold Header**");
|
||||
assert!(full.contains("\x1b[1m\x1b[38;2;145;215;227m"), "Should have sky bold formatting for **Bold Header**");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1817,10 +1817,10 @@ fn test_italic_inside_header() {
|
||||
assert!(!without_ansi.contains('*'), "Should not contain raw * markers, got: {}", without_ansi);
|
||||
|
||||
// Should have header formatting (magenta)
|
||||
assert!(full.contains("\x1b[35m"), "Should have magenta header formatting");
|
||||
assert!(full.contains("\x1b[38;2;138;173;244m"), "Should have blue header formatting");
|
||||
|
||||
// Should have italic formatting (cyan) for the italic text inside
|
||||
assert!(full.contains("\x1b[3;36m"), "Should have cyan italic formatting for *Italic Header*");
|
||||
assert!(full.contains("\x1b[3m\x1b[38;2;125;196;228m"), "Should have sapphire italic formatting for *Italic Header*");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1842,11 +1842,11 @@ fn test_code_inside_header() {
|
||||
let without_ansi = strip_ansi(&full);
|
||||
assert!(!without_ansi.contains('`'), "Should not contain raw backticks, got: {}", without_ansi);
|
||||
|
||||
// Should have header formatting (H3 = cyan in Dracula)
|
||||
assert!(full.contains("\x1b[36m"), "Should have cyan header formatting");
|
||||
// Should have header formatting (H3 = lavender in Catppuccin Macchiato)
|
||||
assert!(full.contains("\x1b[38;2;183;189;248m"), "Should have lavender header formatting");
|
||||
|
||||
// Should have code formatting (orange) for the inline code
|
||||
assert!(full.contains("\x1b[38;2;216;177;114m"), "Should have orange code formatting");
|
||||
assert!(full.contains("\x1b[38;2;245;169;127m"), "Should have peach code formatting");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1870,8 +1870,8 @@ fn test_mixed_formatting_inside_header() {
|
||||
assert!(!without_ansi.contains("*italic*"), "Should not contain raw *italic* markers");
|
||||
|
||||
// Should have both bold and italic formatting
|
||||
assert!(full.contains("\x1b[1;32m"), "Should have green bold formatting");
|
||||
assert!(full.contains("\x1b[3;36m"), "Should have cyan italic formatting");
|
||||
assert!(full.contains("\x1b[1m\x1b[38;2;145;215;227m"), "Should have sky bold formatting");
|
||||
assert!(full.contains("\x1b[3m\x1b[38;2;125;196;228m"), "Should have sapphire italic formatting");
|
||||
}
|
||||
|
||||
/// Helper to strip ANSI escape codes for easier assertion
|
||||
|
||||
Reference in New Issue
Block a user