@@ -11,20 +11,27 @@ const SGR_ITALIC: &str = "\x1b[3m";
1111/// Heading Start and the restorer; restoring just `\x1b[36m` would silently
1212/// drop the bold half.
1313const SGR_HEADING : & str = "\x1b [1;36m" ;
14- /// SGR code for the blockquote dim. Strong End (`\x1b[22m`) clears bold
15- /// *and* faint, and inline Code/Link close with `\x1b[0m` which clears
16- /// every attribute — so the restorer re-applies this when a tag closes
17- /// inside a blockquote.
18- const SGR_BLOCKQUOTE : & str = "\x1b [2m" ;
14+ /// SGR faint. Used for blockquote bodies and for the ambient dim of a
15+ /// streamed thinking block. `\x1b[22m` and `\x1b[0m` both clear faint,
16+ /// so the restorer re-applies it after those resets.
17+ const SGR_FAINT : & str = "\x1b [2m" ;
1918
2019impl UI {
2120 pub fn print_markdown_highlighted ( & self , md : & str ) -> io:: Result < ( ) > {
2221 let mut out = stdout ( ) . lock ( ) ;
23- self . render_markdown_to ( & mut out, md) ?;
22+ self . render_markdown_to ( & mut out, md, false ) ?;
2423 out. flush ( )
2524 }
2625
27- pub ( super ) fn render_markdown_to ( & self , out : & mut impl io:: Write , md : & str ) -> io:: Result < ( ) > {
26+ /// Render `md` to `out` as ANSI-styled text. With `dimmed`, faint is
27+ /// re-applied after each internal SGR reset so streamed thinking
28+ /// stays dim across inline code, links, and headings.
29+ pub ( super ) fn render_markdown_to (
30+ & self ,
31+ out : & mut impl io:: Write ,
32+ md : & str ,
33+ dimmed : bool ,
34+ ) -> io:: Result < ( ) > {
2835 use pulldown_cmark:: { CodeBlockKind , Event , Options , Parser , Tag , TagEnd } ;
2936
3037 // Re-emit any ambient inline styles after a full SGR reset, so nested inline
@@ -35,6 +42,7 @@ impl UI {
3542 italic : bool ,
3643 in_heading : bool ,
3744 in_blockquote : bool ,
45+ dimmed : bool ,
3846 ) -> io:: Result < ( ) > {
3947 if bold {
4048 write ! ( out, "{}" , SGR_BOLD ) ?;
@@ -45,8 +53,16 @@ impl UI {
4553 if in_heading {
4654 write ! ( out, "{}" , SGR_HEADING ) ?;
4755 }
48- if in_blockquote {
49- write ! ( out, "{}" , SGR_BLOCKQUOTE ) ?;
56+ if in_blockquote || dimmed {
57+ write ! ( out, "{}" , SGR_FAINT ) ?;
58+ }
59+ Ok ( ( ) )
60+ }
61+
62+ // Re-apply the ambient faint that a block-level SGR reset clears.
63+ fn restore_faint ( out : & mut impl io:: Write , dimmed : bool ) -> io:: Result < ( ) > {
64+ if dimmed {
65+ write ! ( out, "{}" , SGR_FAINT ) ?;
5066 }
5167 Ok ( ( ) )
5268 }
@@ -69,7 +85,9 @@ impl UI {
6985 }
7086 Event :: End ( TagEnd :: Heading ( _) ) => {
7187 in_heading = false ;
72- writeln ! ( out, "\x1b [0m" ) ?;
88+ write ! ( out, "\x1b [0m" ) ?;
89+ restore_faint ( out, dimmed) ?;
90+ writeln ! ( out) ?;
7391 }
7492 Event :: Start ( Tag :: Strong ) => {
7593 bold = true ;
@@ -78,7 +96,7 @@ impl UI {
7896 Event :: End ( TagEnd :: Strong ) => {
7997 bold = false ;
8098 write ! ( out, "\x1b [22m" ) ?;
81- restore_ambient ( out, bold, italic, in_heading, in_blockquote) ?;
99+ restore_ambient ( out, bold, italic, in_heading, in_blockquote, dimmed ) ?;
82100 }
83101 Event :: Start ( Tag :: Emphasis ) => {
84102 italic = true ;
@@ -99,11 +117,13 @@ impl UI {
99117 Event :: End ( TagEnd :: CodeBlock ) => {
100118 in_code_block = false ;
101119 let highlighted = self . highlighter . highlight_code ( & code_buf, & code_lang) ;
102- writeln ! ( out, "{}" , highlighted) ?;
120+ write ! ( out, "{}" , highlighted) ?;
121+ restore_faint ( out, dimmed) ?;
122+ writeln ! ( out) ?;
103123 }
104124 Event :: Code ( code) => {
105125 write ! ( out, "\x1b [38;2;175;215;255m{}\x1b [0m" , code) ?;
106- restore_ambient ( out, bold, italic, in_heading, in_blockquote) ?;
126+ restore_ambient ( out, bold, italic, in_heading, in_blockquote, dimmed ) ?;
107127 }
108128 Event :: Text ( text) => {
109129 if in_code_block {
@@ -129,17 +149,20 @@ impl UI {
129149 Event :: End ( TagEnd :: List ( _) ) => { }
130150 Event :: Start ( Tag :: Item ) => {
131151 write ! ( out, " {} " , "•" . dimmed( ) ) ?;
152+ restore_faint ( out, dimmed) ?;
132153 }
133154 Event :: End ( TagEnd :: Item ) => {
134155 writeln ! ( out) ?;
135156 }
136157 Event :: Start ( Tag :: BlockQuote ( _) ) => {
137158 in_blockquote = true ;
138- write ! ( out, "{}> " , SGR_BLOCKQUOTE ) ?;
159+ write ! ( out, "{}> " , SGR_FAINT ) ?;
139160 }
140161 Event :: End ( TagEnd :: BlockQuote ( _) ) => {
141162 in_blockquote = false ;
142- writeln ! ( out, "\x1b [0m" ) ?;
163+ write ! ( out, "\x1b [0m" ) ?;
164+ restore_faint ( out, dimmed) ?;
165+ writeln ! ( out) ?;
143166 }
144167 Event :: Start ( Tag :: Link { dest_url, .. } ) => {
145168 // OSC 8 URI terminates on BEL/ESC; bypass the wrapper if dest_url has any control byte.
@@ -151,10 +174,12 @@ impl UI {
151174 }
152175 Event :: End ( TagEnd :: Link ) => {
153176 write ! ( out, "\x1b [0m\x1b ]8;;\x07 " ) ?;
154- restore_ambient ( out, bold, italic, in_heading, in_blockquote) ?;
177+ restore_ambient ( out, bold, italic, in_heading, in_blockquote, dimmed ) ?;
155178 }
156179 Event :: Rule => {
157- writeln ! ( out, "{}" , "─" . repeat( 40 ) . dimmed( ) ) ?;
180+ write ! ( out, "{}" , "─" . repeat( 40 ) . dimmed( ) ) ?;
181+ restore_faint ( out, dimmed) ?;
182+ writeln ! ( out) ?;
158183 }
159184 _ => { }
160185 }
@@ -235,6 +260,8 @@ pub(super) struct MarkdownStreamRenderer {
235260 /// used by the throttle to decide whether enough new content has
236261 /// arrived to justify another full re-render.
237262 last_safe_end : usize ,
263+ /// Passed to the renderer so thinking output keeps its ambient dim.
264+ dimmed : bool ,
238265}
239266
240267impl MarkdownStreamRenderer {
@@ -243,6 +270,15 @@ impl MarkdownStreamRenderer {
243270 buffer : String :: new ( ) ,
244271 committed_lines : 0 ,
245272 last_safe_end : 0 ,
273+ dimmed : false ,
274+ }
275+ }
276+
277+ /// Like [`new`](Self::new), but renders for a dimmed thinking block.
278+ pub ( super ) fn new_dimmed ( ) -> Self {
279+ Self {
280+ dimmed : true ,
281+ ..Self :: new ( )
246282 }
247283 }
248284
@@ -320,7 +356,7 @@ impl MarkdownStreamRenderer {
320356 drop_trailing_blank : bool ,
321357 ) -> io:: Result < ( String , usize ) > {
322358 let mut buf: Vec < u8 > = Vec :: new ( ) ;
323- UI :: shared ( ) . render_markdown_to ( & mut buf, source) ?;
359+ UI :: shared ( ) . render_markdown_to ( & mut buf, source, self . dimmed ) ?;
324360 let rendered = String :: from_utf8_lossy ( & buf) . into_owned ( ) ;
325361 let lines: Vec < & str > = rendered. split_inclusive ( '\n' ) . collect ( ) ;
326362 let mut effective_len = lines. len ( ) ;
@@ -349,7 +385,14 @@ mod render_tests {
349385 fn render ( md : & str ) -> String {
350386 let ui = UI :: new ( ) ;
351387 let mut buf = Vec :: new ( ) ;
352- ui. render_markdown_to ( & mut buf, md) . unwrap ( ) ;
388+ ui. render_markdown_to ( & mut buf, md, false ) . unwrap ( ) ;
389+ String :: from_utf8 ( buf) . unwrap ( )
390+ }
391+
392+ fn render_dimmed ( md : & str ) -> String {
393+ let ui = UI :: new ( ) ;
394+ let mut buf = Vec :: new ( ) ;
395+ ui. render_markdown_to ( & mut buf, md, true ) . unwrap ( ) ;
353396 String :: from_utf8 ( buf) . unwrap ( )
354397 }
355398
@@ -432,7 +475,7 @@ mod render_tests {
432475 . find ( " rest" )
433476 . expect ( "trailing text must be present" ) ;
434477 assert ! (
435- after_strong_end[ ..rest_idx] . contains( SGR_BLOCKQUOTE ) ,
478+ after_strong_end[ ..rest_idx] . contains( SGR_FAINT ) ,
436479 "blockquote dim not restored between Strong End and trailing text; segment={:?}" ,
437480 & after_strong_end[ ..rest_idx]
438481 ) ;
@@ -451,7 +494,7 @@ mod render_tests {
451494 . find ( " rest" )
452495 . expect ( "trailing text must be present" ) ;
453496 assert ! (
454- after_code_reset[ ..rest_idx] . contains( SGR_BLOCKQUOTE ) ,
497+ after_code_reset[ ..rest_idx] . contains( SGR_FAINT ) ,
455498 "blockquote dim not restored between inline Code and trailing text; segment={:?}" ,
456499 & after_code_reset[ ..rest_idx]
457500 ) ;
@@ -468,7 +511,7 @@ mod render_tests {
468511 . find ( " rest" )
469512 . expect ( "trailing text must be present" ) ;
470513 assert ! (
471- after_link_close[ ..rest_idx] . contains( SGR_BLOCKQUOTE ) ,
514+ after_link_close[ ..rest_idx] . contains( SGR_FAINT ) ,
472515 "blockquote dim not restored between Link End and trailing text; segment={:?}" ,
473516 & after_link_close[ ..rest_idx]
474517 ) ;
@@ -490,6 +533,83 @@ mod render_tests {
490533 & after_link_close[ ..rest_idx]
491534 ) ;
492535 }
536+
537+ #[ test]
538+ fn code_in_dimmed_restores_faint ( ) {
539+ // Inline Code closes with \x1b[0m, wiping the thinking block's
540+ // ambient faint; the dimmed renderer must put it back.
541+ let out = render_dimmed ( "the readme uses `gulp watch` then more" ) ;
542+ let after_code_reset = out
543+ . split ( "\x1b [0m" )
544+ . nth ( 1 )
545+ . expect ( "inline Code emits \\ x1b[0m" ) ;
546+ let rest_idx = after_code_reset
547+ . find ( " then more" )
548+ . expect ( "trailing text must be present" ) ;
549+ assert ! (
550+ after_code_reset[ ..rest_idx] . contains( SGR_FAINT ) ,
551+ "faint not restored between inline Code and trailing text; segment={:?}" ,
552+ & after_code_reset[ ..rest_idx]
553+ ) ;
554+ }
555+
556+ #[ test]
557+ fn strong_in_dimmed_restores_faint ( ) {
558+ // Strong End emits \x1b[22m, which clears faint as well as bold.
559+ let out = render_dimmed ( "plain **bold** rest" ) ;
560+ let after_strong_end = out
561+ . split ( "\x1b [22m" )
562+ . nth ( 1 )
563+ . expect ( "Strong End must emit \\ x1b[22m" ) ;
564+ let rest_idx = after_strong_end
565+ . find ( " rest" )
566+ . expect ( "trailing text must be present" ) ;
567+ assert ! (
568+ after_strong_end[ ..rest_idx] . contains( SGR_FAINT ) ,
569+ "faint not restored between Strong End and trailing text; segment={:?}" ,
570+ & after_strong_end[ ..rest_idx]
571+ ) ;
572+ }
573+
574+ #[ test]
575+ fn heading_end_in_dimmed_restores_faint ( ) {
576+ // Heading End emits \x1b[0m; the paragraph after it must come
577+ // back dim.
578+ let out = render_dimmed ( "# Title\n \n body text" ) ;
579+ let body_idx = out. find ( "body text" ) . expect ( "body must be present" ) ;
580+ let last_reset = out[ ..body_idx]
581+ . rfind ( "\x1b [0m" )
582+ . expect ( "Heading End emits \\ x1b[0m" ) ;
583+ assert ! (
584+ out[ last_reset..body_idx] . contains( SGR_FAINT ) ,
585+ "faint not restored between Heading End and body; segment={:?}" ,
586+ & out[ last_reset..body_idx]
587+ ) ;
588+ }
589+
590+ #[ test]
591+ fn code_block_in_dimmed_restores_faint ( ) {
592+ // A code block's syntax highlighting leaves the terminal on a
593+ // non-dim style; the prose after it must come back dim.
594+ let out = render_dimmed ( "```\n let x = 1;\n ```\n \n after the block" ) ;
595+ assert ! (
596+ out. contains( "\x1b [2m\n after the block" ) ,
597+ "faint not restored between code block and trailing text; out={:?}" ,
598+ out
599+ ) ;
600+ }
601+
602+ #[ test]
603+ fn list_item_in_dimmed_restores_faint ( ) {
604+ // The bullet's own styling closes on a reset; the item text
605+ // after it must stay dim.
606+ let out = render_dimmed ( "- first item\n - second item" ) ;
607+ assert ! (
608+ out. contains( "\x1b [2mfirst item" ) ,
609+ "faint not restored between bullet and item text; out={:?}" ,
610+ out
611+ ) ;
612+ }
493613}
494614
495615#[ cfg( test) ]
@@ -499,7 +619,7 @@ mod stream_tests {
499619 fn full_render ( md : & str ) -> String {
500620 let ui = UI :: new ( ) ;
501621 let mut buf = Vec :: new ( ) ;
502- ui. render_markdown_to ( & mut buf, md) . unwrap ( ) ;
622+ ui. render_markdown_to ( & mut buf, md, false ) . unwrap ( ) ;
503623 String :: from_utf8 ( buf) . unwrap ( )
504624 }
505625
0 commit comments