Skip to content

Commit 1baf8e7

Browse files
committed
feat(llm_stream): add font styling support to terminal output formatting
1 parent 76933fc commit 1baf8e7

2 files changed

Lines changed: 72 additions & 3 deletions

File tree

.claude/settings.local.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
"permissions": {
33
"allow": [
44
"Bash(cargo test:*)",
5-
"Bash(cargo build:*)"
5+
"Bash(cargo build:*)",
6+
"WebFetch(domain:docs.rs)"
67
],
78
"deny": []
89
}

crates/llm_stream/src/printer.rs

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use std::fmt::Write;
12
use std::sync::LazyLock;
23

34
use nom::{
@@ -9,9 +10,9 @@ use nom::{
910
};
1011
use syntect::{
1112
easy::HighlightLines,
12-
highlighting::{Style, Theme, ThemeSet},
13+
highlighting::{Color, FontStyle, Style, Theme, ThemeSet},
1314
parsing::{SyntaxReference, SyntaxSet},
14-
util::{as_24_bit_terminal_escaped, LinesWithEndings},
15+
util::LinesWithEndings,
1516
};
1617

1718
static SYNTAX_SET: LazyLock<SyntaxSet> = LazyLock::new(SyntaxSet::load_defaults_newlines);
@@ -33,6 +34,73 @@ pub enum MarkdownElement {
3334
Code { language: String, content: String },
3435
}
3536

37+
#[inline]
38+
fn blend_fg_color(fg: Color, bg: Color) -> Color {
39+
if fg.a == 0xff {
40+
return fg;
41+
}
42+
let ratio = fg.a as u32;
43+
let r = (fg.r as u32 * ratio + bg.r as u32 * (255 - ratio)) / 255;
44+
let g = (fg.g as u32 * ratio + bg.g as u32 * (255 - ratio)) / 255;
45+
let b = (fg.b as u32 * ratio + bg.b as u32 * (255 - ratio)) / 255;
46+
Color {
47+
r: r as u8,
48+
g: g as u8,
49+
b: b as u8,
50+
a: 255,
51+
}
52+
}
53+
54+
/// Formats the styled fragments using 24-bit color terminal escape codes.
55+
/// Meant for debugging and testing.
56+
///
57+
/// This function is currently fairly inefficient in its use of escape codes.
58+
///
59+
/// Note that this does not currently ever un-set the color so that the end of a line will also get
60+
/// highlighted with the background. This means if you might want to use `println!("\x1b[0m");`
61+
/// after to clear the coloring.
62+
///
63+
/// If `bg` is true then the background is also set
64+
pub fn as_24_bit_terminal_escaped(v: &[(Style, &str)], bg: bool) -> String {
65+
let mut s: String = String::new();
66+
for &(ref style, text) in v.iter() {
67+
if bg {
68+
write!(
69+
s,
70+
"\x1b[48;2;{};{};{}m",
71+
style.background.r, style.background.g, style.background.b
72+
)
73+
.unwrap();
74+
}
75+
76+
// Add font style formatting
77+
if style.font_style.contains(FontStyle::BOLD) {
78+
s.push_str("\x1b[1m");
79+
}
80+
if style.font_style.contains(FontStyle::ITALIC) {
81+
s.push_str("\x1b[3m");
82+
}
83+
if style.font_style.contains(FontStyle::UNDERLINE) {
84+
s.push_str("\x1b[4m");
85+
}
86+
87+
let fg = blend_fg_color(style.foreground, style.background);
88+
write!(s, "\x1b[38;2;{};{};{}m{}", fg.r, fg.g, fg.b, text).unwrap();
89+
90+
// Reset only the font styles to avoid bleeding, keeping colors intact
91+
if style.font_style.contains(FontStyle::BOLD) {
92+
s.push_str("\x1b[22m"); // Reset bold
93+
}
94+
if style.font_style.contains(FontStyle::ITALIC) {
95+
s.push_str("\x1b[23m"); // Reset italic
96+
}
97+
if style.font_style.contains(FontStyle::UNDERLINE) {
98+
s.push_str("\x1b[24m"); // Reset underline
99+
}
100+
}
101+
s
102+
}
103+
36104
fn parse_code_fence_start(input: &str) -> IResult<&str, String> {
37105
let (input, _) = tag("```")(input)?;
38106
let (input, language) = opt(not_line_ending)(input)?;

0 commit comments

Comments
 (0)