1use crate::markup;
16use crate::renderable::{Renderable, Segment};
17use crate::text::{Span, Text};
18
19use crossterm::{
20 execute,
21 style::{Attribute, Print, SetAttribute, SetBackgroundColor, SetForegroundColor},
22 terminal,
23};
24use std::io::{self, Write};
25
26fn html_escape(s: &str) -> String {
28 s.replace('&', "&")
29 .replace('<', "<")
30 .replace('>', ">")
31 .replace('"', """)
32}
33
34fn svg_escape(s: &str) -> String {
36 s.replace('&', "&")
37 .replace('<', "<")
38 .replace('>', ">")
39}
40
41#[derive(Debug, Clone)]
43pub struct RenderContext {
44 pub width: usize,
46 pub height: Option<usize>,
48}
49
50impl Default for RenderContext {
51 fn default() -> Self {
52 RenderContext {
53 width: 80,
54 height: None,
55 }
56 }
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
61pub enum ColorSystem {
62 NoColor,
64 #[default]
66 Standard,
67 EightBit,
69 TrueColor,
71 Windows,
73}
74
75#[derive(Debug)]
77pub struct Console {
78 output: ConsoleOutput,
80 width: Option<usize>,
82 force_color: bool,
84 color_enabled: bool,
86 color_system: ColorSystem,
88 markup: bool,
90 emoji: bool,
92 soft_wrap: bool,
94 record: std::sync::Arc<std::sync::atomic::AtomicBool>,
96 recording: std::sync::Arc<std::sync::Mutex<Vec<Segment>>>,
98}
99
100#[derive(Debug, Clone)]
101enum ConsoleOutput {
102 Stdout,
103 Stderr,
104 Buffer(std::sync::Arc<std::sync::Mutex<Vec<u8>>>),
105}
106
107struct BufferWriter {
108 buffer: std::sync::Arc<std::sync::Mutex<Vec<u8>>>,
109}
110
111impl Write for BufferWriter {
112 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
113 let mut lock = self
114 .buffer
115 .lock()
116 .map_err(|e| io::Error::other(e.to_string()))?;
117 lock.extend_from_slice(buf);
118 Ok(buf.len())
119 }
120
121 fn flush(&mut self) -> io::Result<()> {
122 Ok(())
123 }
124}
125
126impl Default for Console {
127 fn default() -> Self {
128 Self::new()
129 }
130}
131
132impl Console {
133 pub fn new() -> Self {
135 let (color_enabled, color_system) = Self::detect_color_system();
136 Console {
137 output: ConsoleOutput::Stdout,
138 width: None,
139 force_color: false,
140 color_enabled,
141 color_system,
142 markup: true,
143 emoji: true,
144 soft_wrap: true,
145 record: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
146 recording: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())),
147 }
148 }
149
150 pub fn stderr() -> Self {
152 let (color_enabled, color_system) = Self::detect_color_system();
153 Console {
154 output: ConsoleOutput::Stderr,
155 width: None,
156 force_color: false,
157 color_enabled,
158 color_system,
159 markup: true,
160 emoji: true,
161 soft_wrap: true,
162 record: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
163 recording: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())),
164 }
165 }
166
167 pub fn capture() -> Self {
171 Console {
172 output: ConsoleOutput::Buffer(std::sync::Arc::new(std::sync::Mutex::new(Vec::new()))),
173 width: Some(80), force_color: true, color_enabled: true,
176 color_system: ColorSystem::TrueColor, markup: true,
178 emoji: true,
179 soft_wrap: true,
180 record: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
181 recording: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())),
182 }
183 }
184
185 pub fn get_captured_output(&self) -> String {
187 match &self.output {
188 ConsoleOutput::Buffer(buf) => {
189 let lock = buf.lock().unwrap();
190 String::from_utf8(lock.clone()).unwrap_or_default()
191 }
192 _ => String::new(),
193 }
194 }
195
196 pub fn width(mut self, width: usize) -> Self {
198 self.width = Some(width);
199 self
200 }
201
202 pub fn force_color(mut self, force: bool) -> Self {
204 self.force_color = force;
205 if force {
206 self.color_enabled = true;
207 if self.color_system == ColorSystem::NoColor {
209 self.color_system = ColorSystem::Standard;
210 }
211 }
212 self
213 }
214
215 pub fn color_system(mut self, system: ColorSystem) -> Self {
217 self.color_system = system;
218 self.color_enabled = system != ColorSystem::NoColor;
220 self
221 }
222
223 pub fn markup(mut self, enabled: bool) -> Self {
225 self.markup = enabled;
226 self
227 }
228
229 pub fn emoji(mut self, enabled: bool) -> Self {
231 self.emoji = enabled;
232 self
233 }
234
235 pub fn soft_wrap(mut self, enabled: bool) -> Self {
237 self.soft_wrap = enabled;
238 self
239 }
240
241 pub fn record(self, enabled: bool) -> Self {
243 self.record
244 .store(enabled, std::sync::atomic::Ordering::Relaxed);
245 self
246 }
247
248 pub fn start_recording(&self) {
250 self.record
251 .store(true, std::sync::atomic::Ordering::Relaxed);
252 if let Ok(mut lock) = self.recording.lock() {
253 lock.clear();
254 }
255 }
256
257 pub fn stop_recording(&self) {
259 self.record
260 .store(false, std::sync::atomic::Ordering::Relaxed);
261 }
262
263 pub fn get_width(&self) -> usize {
265 self.width
266 .unwrap_or_else(|| terminal::size().map(|(w, _)| w as usize).unwrap_or(80))
267 }
268
269 fn detect_color_system() -> (bool, ColorSystem) {
271 if std::env::var("NO_COLOR").is_ok() {
273 return (false, ColorSystem::NoColor);
274 }
275
276 if std::env::var("FORCE_COLOR").is_ok() {
277 return (true, ColorSystem::Standard);
280 }
281
282 if let Ok(colorterm) = std::env::var("COLORTERM") {
284 if colorterm.contains("truecolor") || colorterm.contains("24bit") {
285 return (true, ColorSystem::TrueColor);
286 }
287 }
288
289 if let Ok(term) = std::env::var("TERM") {
291 if term.contains("256color") {
292 return (true, ColorSystem::EightBit);
293 }
294 }
295
296 (true, ColorSystem::Standard)
299 }
300
301 pub fn print(&self, content: &str) {
303 let text = if self.markup {
304 markup::parse(content)
305 } else {
306 Text::plain(content.to_string())
307 };
308
309 self.print_renderable(&text);
310 }
311
312 pub fn print_renderable(&self, renderable: &dyn Renderable) {
314 let context = RenderContext {
315 width: self.get_width(),
316 height: None,
317 };
318
319 let segments = renderable.render(&context);
320 self.write_segments(&segments);
321 }
322
323 pub fn println(&self, content: &str) {
325 self.print(content);
326 self.newline();
327 }
328
329 pub fn print_raw(&self, content: &str) {
334 let text = Text::plain(content.to_string());
335 self.print_renderable(&text);
336 }
337
338 pub fn println_raw(&self, content: &str) {
343 self.print_raw(content);
344 self.newline();
345 }
346
347 pub fn newline(&self) {
349 let _ = self.write_raw("\n");
350 if self.record.load(std::sync::atomic::Ordering::Relaxed) {
352 if let Ok(mut lock) = self.recording.lock() {
353 lock.push(Segment::empty_line());
354 }
355 }
356 }
357
358 pub(crate) fn write_segments(&self, segments: &[Segment]) {
360 if self.record.load(std::sync::atomic::Ordering::Relaxed) {
361 if let Ok(mut lock) = self.recording.lock() {
362 lock.extend_from_slice(segments);
363 }
364 }
365
366 for segment in segments {
367 for span in &segment.spans {
368 self.write_span(span);
369 }
370 if segment.newline {
371 let _ = self.write_raw("\n");
372 }
373 }
374 let _ = self.flush();
375 }
376
377 fn write_span(&self, span: &Span) {
379 if !self.color_enabled || self.color_system == ColorSystem::NoColor || span.style.is_empty()
380 {
381 let _ = self.write_raw(&span.text);
382 return;
383 }
384
385 let mut writer = self.get_writer();
386
387 let process_color = |color: crate::style::Color| -> crossterm::style::Color {
389 match self.color_system {
390 ColorSystem::Standard | ColorSystem::Windows => color.to_standard().to_crossterm(),
391 ColorSystem::EightBit => color.to_ansi256().to_crossterm(),
392 ColorSystem::TrueColor => color.to_crossterm(),
393 ColorSystem::NoColor => crossterm::style::Color::Reset, }
395 };
396
397 if let Some(color) = span.style.foreground {
399 if matches!(
400 self.color_system,
401 ColorSystem::Standard | ColorSystem::Windows
402 ) {
403 let std_color = color.to_standard();
404 let sgr = std_color.to_sgr_fg();
405 if !sgr.is_empty() {
406 let _ = self.write_raw(&sgr);
407 } else {
408 let _ = execute!(writer, SetForegroundColor(std_color.to_crossterm()));
409 }
410 } else {
411 let _ = execute!(writer, SetForegroundColor(process_color(color)));
412 }
413 }
414
415 if let Some(color) = span.style.background {
417 if matches!(
418 self.color_system,
419 ColorSystem::Standard | ColorSystem::Windows
420 ) {
421 let std_color = color.to_standard();
422 let sgr = std_color.to_sgr_bg();
423 if !sgr.is_empty() {
424 let _ = self.write_raw(&sgr);
425 } else {
426 let _ = execute!(writer, SetBackgroundColor(std_color.to_crossterm()));
427 }
428 } else {
429 let _ = execute!(writer, SetBackgroundColor(process_color(color)));
430 }
431 }
432
433 if span.style.bold {
435 let _ = execute!(writer, SetAttribute(Attribute::Bold));
436 }
437 if span.style.dim {
438 let _ = execute!(writer, SetAttribute(Attribute::Dim));
439 }
440 if span.style.italic {
441 let _ = execute!(writer, SetAttribute(Attribute::Italic));
442 }
443 if span.style.underline {
444 let _ = execute!(writer, SetAttribute(Attribute::Underlined));
445 }
446 if span.style.blink {
447 let _ = execute!(writer, SetAttribute(Attribute::SlowBlink));
448 }
449 if span.style.reverse {
450 let _ = execute!(writer, SetAttribute(Attribute::Reverse));
451 }
452 if span.style.hidden {
453 let _ = execute!(writer, SetAttribute(Attribute::Hidden));
454 }
455 if span.style.strikethrough {
456 let _ = execute!(writer, SetAttribute(Attribute::CrossedOut));
457 }
458
459 let _ = execute!(writer, Print(&span.text));
461
462 let _ = execute!(writer, SetAttribute(Attribute::Reset));
464 }
465
466 fn get_writer(&self) -> Box<dyn Write> {
468 match &self.output {
469 ConsoleOutput::Stdout => Box::new(io::stdout()),
470 ConsoleOutput::Stderr => Box::new(io::stderr()),
471 ConsoleOutput::Buffer(buf) => Box::new(BufferWriter {
472 buffer: buf.clone(),
473 }),
474 }
475 }
476
477 fn write_raw(&self, s: &str) -> io::Result<()> {
479 match &self.output {
480 ConsoleOutput::Stdout => {
481 let mut stdout = io::stdout();
482 stdout.write_all(s.as_bytes())
483 }
484 ConsoleOutput::Stderr => {
485 let mut stderr = io::stderr();
486 stderr.write_all(s.as_bytes())
487 }
488 ConsoleOutput::Buffer(buf) => {
489 let mut lock = buf.lock().map_err(|e| io::Error::other(e.to_string()))?;
490 lock.extend_from_slice(s.as_bytes());
491 Ok(())
492 }
493 }
494 }
495
496 fn flush(&self) -> io::Result<()> {
498 match &self.output {
499 ConsoleOutput::Stdout => io::stdout().flush(),
500 ConsoleOutput::Stderr => io::stderr().flush(),
501 ConsoleOutput::Buffer(_) => Ok(()),
502 }
503 }
504
505 pub fn clear(&self) {
507 let mut writer = self.get_writer();
508 let _ = execute!(
509 writer,
510 crossterm::terminal::Clear(crossterm::terminal::ClearType::All),
511 crossterm::cursor::MoveTo(0, 0)
512 );
513 }
514
515 pub fn show_cursor(&self, show: bool) {
517 let mut writer = self.get_writer();
518 if show {
519 let _ = execute!(writer, crossterm::cursor::Show);
520 } else {
521 let _ = execute!(writer, crossterm::cursor::Hide);
522 }
523 }
524
525 pub fn move_cursor_up(&self, n: u16) {
527 if n > 0 {
528 let mut writer = self.get_writer();
529 let _ = execute!(writer, crossterm::cursor::MoveUp(n));
530 }
531 }
532
533 pub fn move_cursor_down(&self, n: u16) {
535 if n > 0 {
536 let mut writer = self.get_writer();
537 let _ = execute!(writer, crossterm::cursor::MoveDown(n));
538 }
539 }
540
541 pub fn clear_line(&self) {
543 let mut writer = self.get_writer();
544 let _ = execute!(
545 writer,
546 crossterm::terminal::Clear(crossterm::terminal::ClearType::CurrentLine),
547 crossterm::cursor::MoveToColumn(0)
548 );
549 }
550
551 pub fn rule(&self, title: &str) {
553 let _width = self.get_width();
554 let rule = crate::rule::Rule::new(title);
555 self.print_renderable(&rule);
556 self.newline();
557 }
558
559 #[cfg(feature = "syntax")]
564 pub fn print_json(&self, json_str: &str) {
565 let syntax = crate::syntax::Syntax::new(json_str, "json");
566 self.print_renderable(&syntax);
567 self.newline();
568 }
569
570 pub fn print_debug<T: std::fmt::Debug>(&self, obj: &T) {
574 let content = format!("{:#?}", obj);
575
576 #[cfg(feature = "syntax")]
577 {
578 let syntax = crate::syntax::Syntax::new(&content, "rust");
579 self.print_renderable(&syntax);
580 }
581
582 #[cfg(not(feature = "syntax"))]
583 {
584 let text = Text::plain(content);
586 self.print_renderable(&text);
587 }
588
589 self.newline();
590 }
591
592 pub fn export_text(&self, renderable: &dyn Renderable) -> String {
596 let context = RenderContext {
597 width: self.get_width(),
598 height: None,
599 };
600 let segments = renderable.render(&context);
601 self.segments_to_text(&segments)
602 }
603
604 fn segments_to_text(&self, segments: &[Segment]) -> String {
605 let mut result = String::new();
606 for segment in segments {
607 result.push_str(&segment.plain_text());
608 if segment.newline {
609 result.push('\n');
610 }
611 }
612 result
613 }
614
615 pub fn export_html(&self, renderable: &dyn Renderable) -> String {
619 let context = RenderContext {
620 width: self.get_width(),
621 height: None,
622 };
623 let segments = renderable.render(&context);
624 self.segments_to_html(&segments)
625 }
626
627 pub fn save_html(&self, path: &str) -> io::Result<()> {
629 let segments = self.recording.lock().unwrap();
630 let html = self.segments_to_html(&segments);
631 std::fs::write(path, html)
632 }
633
634 fn segments_to_html(&self, segments: &[Segment]) -> String {
635 let mut html = String::from("<pre style=\"font-family: monospace; background: #1e1e1e; color: #d4d4d4; padding: 1em;\">\n");
636
637 for segment in segments {
638 for span in &segment.spans {
639 let style_css = span.style.to_css();
640 if style_css.is_empty() {
641 html.push_str(&html_escape(&span.text));
642 } else {
643 html.push_str(&format!(
644 "<span style=\"{}\">{}</span>",
645 style_css,
646 html_escape(&span.text)
647 ));
648 }
649 }
650 if segment.newline {
651 html.push('\n');
652 }
653 }
654
655 html.push_str("</pre>");
656 html
657 }
658
659 pub fn export_svg(&self, renderable: &dyn Renderable) -> String {
663 let context = RenderContext {
664 width: self.get_width(),
665 height: None,
666 };
667 let segments = renderable.render(&context);
668 self.segments_to_svg(&segments)
669 }
670
671 pub fn save_svg(&self, path: &str) -> io::Result<()> {
673 let segments = self.recording.lock().unwrap();
674 let svg = self.segments_to_svg(&segments);
675 std::fs::write(path, svg)
676 }
677
678 fn segments_to_svg(&self, segments: &[Segment]) -> String {
679 let char_width = 9.6; let line_height = 20.0;
681 let padding = 10.0;
682
683 let mut lines: Vec<String> = Vec::new();
684 let mut current_line = String::new();
685
686 for segment in segments {
687 for span in &segment.spans {
688 current_line.push_str(&span.text);
689 }
690 if segment.newline {
691 lines.push(std::mem::take(&mut current_line));
692 }
693 }
694 if !current_line.is_empty() {
695 lines.push(current_line);
696 }
697
698 let max_chars = lines.iter().map(|l| l.len()).max().unwrap_or(80);
699 let width = (max_chars as f64 * char_width) + padding * 2.0;
700 let height = (lines.len() as f64 * line_height) + padding * 2.0;
701
702 let mut svg = format!(
703 "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 {:.0} {:.0}\">\n",
704 width, height
705 );
706 svg.push_str(" <rect width=\"100%\" height=\"100%\" fill=\"#1e1e1e\"/>\n");
707 svg.push_str(" <text font-family=\"monospace\" font-size=\"14\" fill=\"#d4d4d4\">\n");
708
709 for (i, line) in lines.iter().enumerate() {
710 let y = padding + (i as f64 + 1.0) * line_height;
711 svg.push_str(&format!(
712 " <tspan x=\"{}\" y=\"{:.1}\">{}</tspan>\n",
713 padding,
714 y,
715 svg_escape(line)
716 ));
717 }
718
719 svg.push_str(" </text>\n</svg>");
720 svg
721 }
722}
723
724#[derive(Debug)]
726pub struct CapturedOutput {
727 segments: Vec<Segment>,
728}
729
730impl CapturedOutput {
731 pub fn new() -> Self {
733 CapturedOutput {
734 segments: Vec::new(),
735 }
736 }
737
738 pub fn plain_text(&self) -> String {
740 let mut result = String::new();
741 for segment in &self.segments {
742 result.push_str(&segment.plain_text());
743 if segment.newline {
744 result.push('\n');
745 }
746 }
747 result
748 }
749}
750
751impl Default for CapturedOutput {
752 fn default() -> Self {
753 Self::new()
754 }
755}
756
757#[cfg(test)]
758mod tests {
759 use super::*;
760
761 #[test]
762 fn test_console_default_width() {
763 let console = Console::new().width(80);
764 assert_eq!(console.get_width(), 80);
765 }
766
767 #[test]
768 fn test_render_context_default() {
769 let context = RenderContext::default();
770 assert_eq!(context.width, 80);
771 }
772
773 #[test]
774 fn test_force_color() {
775 let console = Console::new().force_color(true);
776 assert!(console.force_color);
777 assert!(console.color_enabled);
778 }
779}