1use std::fmt;
8use std::io::{self, Write};
9use std::sync::{Arc, Mutex};
10
11use crate::align::AlignMethod;
12use crate::color::{Color, ColorSystem};
13use crate::segment::Segment;
14use crate::style::Style;
15use crate::text::Text;
16use crate::theme::Theme;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub struct ConsoleDimensions {
25 pub width: usize,
26 pub height: usize,
27}
28
29impl ConsoleDimensions {
30 pub fn detect() -> Self {
32 if let Some((w, h)) = terminal_size::terminal_size() {
33 Self {
34 width: w.0 as usize,
35 height: h.0 as usize,
36 }
37 } else {
38 Self {
39 width: 80,
40 height: 25,
41 }
42 }
43 }
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
52pub enum OverflowMethod {
53 Fold,
55 Crop,
57 Ellipsis,
59 Ignore,
61}
62
63#[derive(Debug, Clone)]
69pub struct ConsoleOptions {
70 pub size: ConsoleDimensions,
72 pub is_terminal: bool,
74 pub encoding: String,
76 pub min_width: usize,
78 pub max_width: usize,
80 pub max_height: usize,
82 pub justify: Option<AlignMethod>,
84 pub overflow: Option<OverflowMethod>,
86 pub no_wrap: bool,
88 pub ascii_only: bool,
90 pub markup: bool,
92 pub highlight: bool,
94 pub height: Option<usize>,
96 pub legacy_windows: bool,
98}
99
100impl Default for ConsoleOptions {
101 fn default() -> Self {
102 Self {
103 size: ConsoleDimensions::detect(),
104 is_terminal: true,
105 encoding: "utf-8".into(),
106 min_width: 1,
107 max_width: 80,
108 max_height: 25,
109 justify: None,
110 overflow: None,
111 no_wrap: false,
112 ascii_only: false,
113 markup: true,
114 highlight: true,
115 height: None,
116 legacy_windows: false,
117 }
118 }
119}
120
121impl ConsoleOptions {
122 pub fn update_width(&self, max_width: usize) -> Self {
124 let mut opts = self.clone();
125 opts.max_width = max_width;
126 opts
127 }
128
129 pub fn update_height(&self, height: usize) -> Self {
131 let mut opts = self.clone();
132 opts.height = Some(height);
133 opts
134 }
135
136 pub fn shrink_width(&self, amount: usize) -> Self {
138 let mut opts = self.clone();
139 opts.max_width = opts.max_width.saturating_sub(amount);
140 opts
141 }
142}
143
144#[derive(Clone)]
153pub enum RenderItem {
154 Segment(Segment),
156 Nested(DynRenderable),
158}
159
160impl fmt::Debug for RenderItem {
161 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
162 match self {
163 Self::Segment(s) => write!(f, "Segment({})", &s.text),
164 Self::Nested(_) => write!(f, "Nested(...)"),
165 }
166 }
167}
168
169impl From<Segment> for RenderItem {
170 fn from(s: Segment) -> Self { Self::Segment(s) }
171}
172
173impl From<DynRenderable> for RenderItem {
174 fn from(r: DynRenderable) -> Self { Self::Nested(r) }
175}
176
177#[derive(Debug, Clone)]
180pub struct RenderResult {
181 pub lines: Vec<Vec<Segment>>,
183 pub items: Vec<RenderItem>,
186}
187
188impl RenderResult {
189 pub fn new() -> Self {
191 Self { lines: Vec::new(), items: Vec::new() }
192 }
193
194 pub fn from_text(text: &str) -> Self {
198 Self {
199 lines: vec![vec![Segment::new(text)]],
200 items: vec![RenderItem::Segment(Segment::new(text))],
201 }
202 }
203
204 pub fn from_segments(segments: Vec<Segment>) -> Self {
206 let items: Vec<RenderItem> = segments.iter().map(|s| RenderItem::Segment(s.clone())).collect();
207 Self { lines: vec![segments], items }
208 }
209
210 pub fn from_lines(lines: Vec<Vec<Segment>>) -> Self {
212 Self { lines, items: Vec::new() }
213 }
214
215 pub fn from_items(items: Vec<RenderItem>) -> Self {
217 Self { lines: Vec::new(), items }
218 }
219
220 pub fn push_item(&mut self, item: impl Into<RenderItem>) {
222 self.items.push(item.into());
223 }
224
225 pub fn push_renderable(&mut self, r: impl Renderable + Send + Sync + 'static) {
227 self.items.push(RenderItem::Nested(DynRenderable::new(r)));
228 }
229
230 pub fn flatten(&self, options: &ConsoleOptions) -> Vec<Segment> {
233 let mut out: Vec<Segment> = Vec::new();
234 flatten_items(&self.items, options, &mut out);
235 if out.is_empty() {
237 for line in &self.lines {
238 for seg in line {
239 out.push(seg.clone());
240 }
241 }
242 }
243 out
244 }
245
246 pub fn to_ansi(&self) -> String {
248 let mut out = String::new();
249 if !self.items.is_empty() {
251 let flat = self.flatten(&ConsoleOptions::default());
252 for seg in &flat {
253 out.push_str(&seg.to_ansi());
254 }
255 } else {
256 for line in &self.lines {
257 for seg in line {
258 out.push_str(&seg.to_ansi());
259 }
260 }
261 }
262 out
263 }
264}
265
266fn flatten_items(items: &[RenderItem], options: &ConsoleOptions, out: &mut Vec<Segment>) {
268 for item in items {
269 match item {
270 RenderItem::Segment(seg) => out.push(seg.clone()),
271 RenderItem::Nested(renderable) => {
272 let nested = renderable.render(options);
273 flatten_items(&nested.items, options, out);
274 }
275 }
276 }
277}
278
279pub trait Renderable {
283 fn render(&self, options: &ConsoleOptions) -> RenderResult;
288
289 fn measure(&self, _options: &ConsoleOptions) -> Option<crate::measure::Measurement> {
292 None
293 }
294}
295
296impl Renderable for String {
300 fn render(&self, options: &ConsoleOptions) -> RenderResult {
301 self.as_str().render(options)
302 }
303}
304
305impl Renderable for &str {
307 fn render(&self, _options: &ConsoleOptions) -> RenderResult {
308 RenderResult::from_text(self)
309 }
310}
311
312impl Renderable for Text {
314 fn render(&self, _options: &ConsoleOptions) -> RenderResult {
315 let rendered = self.render();
316 let lines: Vec<Vec<Segment>> = rendered
318 .lines()
319 .map(|l| vec![Segment::new(l)])
320 .collect();
321 RenderResult { lines, items: Vec::new() }
322 }
323}
324
325#[derive(Clone)]
330pub struct DynRenderable {
331 inner: Arc<dyn Renderable + Send + Sync>,
332}
333
334impl DynRenderable {
335 pub fn new(r: impl Renderable + Send + Sync + 'static) -> Self {
337 Self { inner: Arc::new(r) }
338 }
339}
340
341impl fmt::Debug for DynRenderable {
342 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
343 f.debug_struct("DynRenderable").finish()
344 }
345}
346
347impl Renderable for DynRenderable {
349 fn render(&self, options: &ConsoleOptions) -> RenderResult {
350 self.inner.render(options)
351 }
352
353 fn measure(&self, options: &ConsoleOptions) -> Option<crate::measure::Measurement> {
354 self.inner.measure(options)
355 }
356}
357
358#[derive(Debug, Clone)]
362pub struct Group {
363 pub children: Vec<DynRenderable>,
365}
366
367impl Group {
368 pub fn new() -> Self {
370 Self { children: Vec::new() }
371 }
372
373 pub fn add(&mut self, renderable: impl Renderable + Send + Sync + 'static) {
375 self.children.push(DynRenderable::new(renderable));
376 }
377}
378
379impl Renderable for Group {
381 fn render(&self, options: &ConsoleOptions) -> RenderResult {
382 let mut all_lines: Vec<Vec<Segment>> = Vec::new();
383 for child in &self.children {
384 let result = child.render(options);
385 all_lines.extend(result.lines);
386 }
387 RenderResult { lines: all_lines, items: Vec::new() }
388 }
389}
390
391struct CaptureWriter {
397 buf: Arc<Mutex<Vec<u8>>>,
398}
399
400impl Write for CaptureWriter {
401 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
402 let mut data = self.buf.lock().unwrap();
403 data.extend_from_slice(buf);
404 Ok(buf.len())
405 }
406 fn flush(&mut self) -> io::Result<()> {
407 Ok(())
408 }
409}
410
411pub struct Capture {
413 buf: Arc<Mutex<Vec<u8>>>,
414}
415
416impl Capture {
417 pub fn new(_console: &Console) -> Self {
419 Self { buf: Arc::new(Mutex::new(Vec::new())) }
420 }
421
422 pub fn get(&self) -> String {
424 let data = self.buf.lock().unwrap();
425 String::from_utf8_lossy(&data).to_string()
426 }
427}
428
429pub use crate::pager::{Pager, PagerContext, SystemPager};
431
432#[derive(Debug, Clone, PartialEq, Eq)]
438pub enum CaptureError {
439 AlreadyCapturing,
441 NotCapturing,
443 InvalidUtf8,
445}
446
447impl fmt::Display for CaptureError {
448 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
449 match self {
450 Self::AlreadyCapturing => write!(f, "capture already in progress"),
451 Self::NotCapturing => write!(f, "no capture active"),
452 Self::InvalidUtf8 => write!(f, "captured output is not valid UTF-8"),
453 }
454 }
455}
456
457impl std::error::Error for CaptureError {}
458
459pub struct NewLine;
465
466impl Renderable for NewLine {
467 fn render(&self, _options: &ConsoleOptions) -> RenderResult {
468 RenderResult::from_text("\n")
469 }
470}
471
472pub struct NoChange;
474
475impl Renderable for NoChange {
476 fn render(&self, _options: &ConsoleOptions) -> RenderResult {
477 RenderResult::new()
478 }
479}
480
481pub struct RenderHook {
487 hook: Box<dyn Fn(&[Vec<Segment>]) -> Vec<Vec<Segment>> + Send>,
488}
489
490impl RenderHook {
491 pub fn new<F: Fn(&[Vec<Segment>]) -> Vec<Vec<Segment>> + Send + 'static>(f: F) -> Self {
493 Self { hook: Box::new(f) }
494 }
495
496 pub fn apply(&self, lines: &[Vec<Segment>]) -> Vec<Vec<Segment>> {
498 (self.hook)(lines)
499 }
500}
501
502impl fmt::Debug for RenderHook {
503 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
504 f.debug_struct("RenderHook").finish()
505 }
506}
507
508pub struct ThemeContext {
517 console_ptr: *mut Console,
518 previous_theme: Theme,
519}
520
521impl ThemeContext {
526 pub(crate) fn new(console: &mut Console, previous_theme: Theme) -> Self {
528 Self {
529 console_ptr: console as *mut Console,
530 previous_theme,
531 }
532 }
533}
534
535impl Drop for ThemeContext {
536 fn drop(&mut self) {
537 unsafe {
538 (*self.console_ptr).theme = std::mem::take(&mut self.previous_theme);
539 }
540 }
541}
542
543pub struct Console {
549 pub file: Box<dyn Write + Send>,
551 pub color_system: ColorSystem,
553 pub theme: Theme,
555 pub options: ConsoleOptions,
557 width: Option<usize>,
559 height: Option<usize>,
561 is_terminal: bool,
563 pub quiet: bool,
565 pub soft_wrap: bool,
567 alt_screen: bool,
569 cursor_visible: bool,
571 render_hooks: Vec<RenderHook>,
573 capture_buf: Option<Arc<Mutex<Vec<u8>>>>,
575 saved_file: Option<Box<dyn Write + Send>>,
577}
578
579impl Console {
580 pub fn new() -> Self {
582 let is_terminal = atty::is(atty::Stream::Stdout);
583 let color_system = detect_color_system();
584
585 let size = ConsoleDimensions::detect();
586
587 Self {
588 file: Box::new(io::stdout()) as Box<dyn Write + Send>,
589 color_system,
590 theme: crate::theme::default_theme(),
591 options: ConsoleOptions {
592 size,
593 is_terminal,
594 max_width: size.width,
595 max_height: size.height,
596 ..Default::default()
597 },
598 width: None,
599 height: None,
600 is_terminal,
601 quiet: false,
602 soft_wrap: false,
603 alt_screen: false,
604 cursor_visible: true,
605 render_hooks: Vec::new(),
606 capture_buf: None,
607 saved_file: None,
608 }
609 }
610
611 pub fn with_file(file: Box<dyn Write + Send>) -> Self {
613 let _is_terminal = false;
614 Self {
615 file,
616 color_system: ColorSystem::Standard,
617 theme: crate::theme::default_theme(),
618 options: ConsoleOptions {
619 size: ConsoleDimensions { width: 80, height: 25 },
620 is_terminal: false,
621 max_width: 80,
622 max_height: 25,
623 ..Default::default()
624 },
625 width: None,
626 height: None,
627 is_terminal: false,
628 quiet: false,
629 soft_wrap: false,
630 alt_screen: false,
631 cursor_visible: true,
632 render_hooks: Vec::new(),
633 capture_buf: None,
634 saved_file: None,
635 }
636 }
637
638 pub fn set_width(&mut self, width: usize) {
640 self.width = Some(width);
641 self.options.max_width = width;
642 }
643
644 pub fn set_height(&mut self, height: usize) {
646 self.height = Some(height);
647 self.options.max_height = height;
648 }
649
650 pub fn width(&self) -> usize {
652 self.width.unwrap_or(self.options.size.width)
653 }
654
655 pub fn height(&self) -> usize {
657 self.height.unwrap_or(self.options.size.height)
658 }
659
660 pub fn render_lines(
662 &self,
663 renderable: &dyn Renderable,
664 options: &ConsoleOptions,
665 style: Option<&Style>,
666 _pad: bool,
667 ) -> Vec<Vec<Segment>> {
668 let result = renderable.render(options);
669
670 if let Some(st) = style {
671 result
672 .lines
673 .into_iter()
674 .map(|line| {
675 line.into_iter()
676 .map(|seg| {
677 let new_style = if let Some(ref s) = seg.style {
678 s.combine(st)
679 } else {
680 st.clone()
681 };
682 Segment::styled(seg.text, new_style)
683 })
684 .collect()
685 })
686 .collect()
687 } else {
688 result.lines
689 }
690 }
691
692 pub fn get_style(&self, name: &str, default: &str) -> Option<Style> {
694 self.theme
695 .get(name)
696 .cloned()
697 .or_else(|| {
698 if !default.is_empty() {
699 Some(Style::from_str(default))
700 } else {
701 None
702 }
703 })
704 }
705
706 pub fn render_str(&self, text: &str, style: &str) -> Text {
708 let st = self.get_style(style, "");
709 let mut t = Text::new(text);
710 if let Some(s) = st {
711 t = t.style(s);
712 }
713 t
714 }
715
716 pub fn print(&mut self, objects: &[&dyn Renderable], sep: &str, end: &str) {
723 if self.quiet { return; }
724 let mut first = true;
725 for obj in objects {
726 if !first {
727 let _ = write!(self.file, "{sep}");
728 }
729 first = false;
730 let result = obj.render(&self.options);
731 let ansi = result.to_ansi();
732 let _ = write!(self.file, "{ansi}");
733 }
734 let _ = write!(self.file, "{end}");
735 let _ = self.file.flush();
736 }
737
738 pub fn println(&mut self, renderable: &dyn Renderable) {
740 if self.quiet { return; }
741 let result = renderable.render(&self.options);
742 let ansi = result.to_ansi();
743 let _ = writeln!(self.file, "{ansi}");
744 let _ = self.file.flush();
745 }
746
747 pub fn print_str(&mut self, text: &str) {
750 if self.quiet { return; }
751 let ansi = if self.options.markup {
752 let parsed = crate::markup::render(text);
753 parsed.render()
754 } else {
755 text.to_string()
756 };
757 let _ = write!(self.file, "{ansi}");
758 let _ = self.file.flush();
759 }
760
761 pub fn print_json(&mut self, data: &serde_json::Value) {
763 if self.quiet { return; }
764 let formatted = crate::json::render_json(data);
765 let result = formatted.render(&self.options);
766 let ansi = result.to_ansi();
767 let _ = writeln!(self.file, "{ansi}");
768 let _ = self.file.flush();
769 }
770
771 pub fn clear(&mut self) {
773 if self.quiet { return; }
774 let _ = write!(self.file, "\x1b[2J\x1b[H");
775 let _ = self.file.flush();
776 }
777
778 pub fn show_cursor(&mut self) {
780 self.cursor_visible = true;
781 let _ = write!(self.file, "\x1b[?25h");
782 let _ = self.file.flush();
783 }
784
785 pub fn hide_cursor(&mut self) {
787 self.cursor_visible = false;
788 let _ = write!(self.file, "\x1b[?25l");
789 let _ = self.file.flush();
790 }
791
792 pub fn set_window_title(&mut self, title: &str) {
794 let _ = write!(self.file, "\x1b]0;{title}\x07");
795 let _ = self.file.flush();
796 }
797
798 pub fn color_ansi(&self, color: &Color) -> String {
800 let downgraded = color.downgrade(self.color_system);
801 downgraded.to_string()
802 }
803
804 pub fn render(&self, renderable: &dyn Renderable, options: &ConsoleOptions) -> Vec<Segment> {
811 let result = renderable.render(options);
812 result.flatten(options)
813 }
814
815 pub fn measure(&self, renderable: &dyn Renderable, options: &ConsoleOptions) -> crate::measure::Measurement {
818 if let Some(m) = renderable.measure(options) {
819 return m;
820 }
821 let segments = self.render(renderable, options);
822 let max_w = segments.iter()
823 .map(|s| s.cell_length())
824 .max()
825 .unwrap_or(0);
826 crate::measure::Measurement::new(max_w, options.max_width)
827 }
828
829 pub fn rule(
834 &mut self,
835 title: impl Into<String>,
836 characters: Option<&str>,
837 style: Option<Style>,
838 align: Option<AlignMethod>,
839 ) {
840 if self.quiet { return; }
841 let mut rule = crate::rule::Rule::new().title(title);
842 if let Some(chars) = characters { rule = rule.characters(chars); }
843 if let Some(st) = style { rule = rule.style(st); }
844 if let Some(a) = align { rule = rule.align(a); }
845 let result = rule.render(&self.options);
846 let ansi = result.to_ansi();
847 let _ = write!(self.file, "{ansi}");
848 let _ = self.file.flush();
849 }
850
851 pub fn bell(&mut self) {
853 if self.quiet { return; }
854 let _ = write!(self.file, "\x07");
855 let _ = self.file.flush();
856 }
857
858 pub fn line(&mut self, count: usize) {
860 if self.quiet { return; }
861 for _ in 0..count {
862 let _ = writeln!(self.file);
863 }
864 let _ = self.file.flush();
865 }
866
867 pub fn log(&mut self, objects: &[&dyn Renderable]) {
869 if self.quiet { return; }
870 let now = chrono::Local::now();
871 let time_str = format!("[{}]", now.format("%H:%M:%S"));
872 let _ = write!(self.file, "{} ", Style::new().dim(true).to_ansi());
873 let _ = write!(self.file, "{time_str} ");
874 let _ = write!(self.file, "{}", Style::new().reset_ansi());
875 self.print(objects, " ", "\n");
876 }
877
878 pub fn push_theme(&mut self, theme: Theme) {
882 let mut new_theme = theme.clone();
883 new_theme.inherit = Some(Box::new(self.theme.clone()));
884 self.theme = new_theme;
885 }
886
887 pub fn pop_theme(&mut self) {
889 if let Some(ref inherit) = self.theme.inherit {
890 self.theme = *inherit.clone();
891 }
892 }
893
894 pub fn export_html(&self, renderable: &dyn Renderable) -> String {
900 let result = renderable.render(&self.options);
901 let ansi = result.to_ansi();
902 crate::export::export_html(&crate::export::ExportHtmlOptions {
903 code: crate::export::strip_ansi_escapes(&ansi),
904 ..Default::default()
905 })
906 }
907
908 pub fn save_html(&self, path: impl AsRef<std::path::Path>, renderable: &dyn Renderable) -> std::io::Result<()> {
910 let html = self.export_html(renderable);
911 crate::export::save_html(path, &crate::export::ExportHtmlOptions {
912 code: html,
913 ..Default::default()
914 })
915 }
916
917 pub fn export_svg(&self, renderable: &dyn Renderable) -> String {
919 let result = renderable.render(&self.options);
920 let ansi = result.to_ansi();
921 crate::export::export_svg(&crate::export::ExportSvgOptions {
922 code: crate::export::strip_ansi_escapes(&ansi),
923 ..Default::default()
924 })
925 }
926
927 pub fn save_svg(&self, path: impl AsRef<std::path::Path>, renderable: &dyn Renderable) -> std::io::Result<()> {
929 let svg = self.export_svg(renderable);
930 crate::export::save_svg(path, &crate::export::ExportSvgOptions {
931 code: svg,
932 ..Default::default()
933 })
934 }
935
936 pub fn export_text(&self, renderable: &dyn Renderable) -> String {
938 let result = renderable.render(&self.options);
939 let ansi = result.to_ansi();
940 crate::export::export_text(&crate::export::ExportTextOptions {
941 text: ansi,
942 strip_ansi: true,
943 })
944 }
945
946 pub fn save_text(&self, path: impl AsRef<std::path::Path>, renderable: &dyn Renderable) -> std::io::Result<()> {
948 let text = self.export_text(renderable);
949 crate::export::save_text(path, &crate::export::ExportTextOptions {
950 text,
951 strip_ansi: false,
952 })
953 }
954
955 pub fn set_quiet(&mut self, quiet: bool) {
959 self.quiet = quiet;
960 }
961
962 pub fn quiet(mut self, quiet: bool) -> Self {
964 self.quiet = quiet;
965 self
966 }
967
968 pub fn set_soft_wrap(&mut self, soft_wrap: bool) {
970 self.soft_wrap = soft_wrap;
971 }
972
973 pub fn soft_wrap(mut self, soft_wrap: bool) -> Self {
975 self.soft_wrap = soft_wrap;
976 self
977 }
978
979 pub fn input(&mut self, prompt: &str, password: bool) -> String {
987 let _ = write!(self.file, "{prompt}");
988 let _ = self.file.flush();
989
990 if password {
991 self.read_password()
992 } else {
993 let mut input = String::new();
994 let _ = io::stdin().read_line(&mut input);
995 input.trim().to_string()
996 }
997 }
998
999 fn read_password(&mut self) -> String {
1001 use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
1002 use std::io::Read;
1003
1004 match enable_raw_mode() {
1005 Ok(()) => {
1006 let stdin = io::stdin();
1007 let mut handle = stdin.lock();
1008 let mut buf = [0u8; 1];
1009 let mut password = String::new();
1010
1011 loop {
1012 match handle.read_exact(&mut buf) {
1013 Ok(()) => match buf[0] {
1014 b'\r' | b'\n' => {
1015 let _ = writeln!(self.file);
1016 let _ = self.file.flush();
1017 break;
1018 }
1019 b'\x03' => {
1020 let _ = writeln!(self.file);
1022 let _ = self.file.flush();
1023 break;
1024 }
1025 b'\x7f' | b'\x08' => {
1026 password.pop();
1028 }
1029 c => {
1030 password.push(c as char);
1031 let _ = write!(self.file, "*");
1032 let _ = self.file.flush();
1033 }
1034 },
1035 Err(_) => break,
1036 }
1037 }
1038 let _ = disable_raw_mode();
1039 password
1040 }
1041 Err(_) => {
1042 let mut input = String::new();
1044 let _ = io::stdin().read_line(&mut input);
1045 input.trim().to_string()
1046 }
1047 }
1048 }
1049
1050 pub fn screen(&mut self) -> crate::screen::ScreenContext {
1056 let mut ctx = crate::screen::ScreenContext::new();
1057 ctx.enter();
1058 ctx
1059 }
1060
1061 pub fn set_alt_screen(&mut self, enable: bool) {
1064 self.alt_screen = enable;
1065 if enable {
1066 let _ = write!(self.file, "\x1b[?1049h");
1067 } else {
1068 let _ = write!(self.file, "\x1b[?1049l");
1069 }
1070 let _ = self.file.flush();
1071 }
1072
1073 pub fn is_terminal(&self) -> bool {
1075 self.is_terminal
1076 }
1077
1078 pub fn set_size(&mut self, width: usize, height: usize) {
1080 self.width = Some(width);
1081 self.height = Some(height);
1082 self.options.max_width = width;
1083 self.options.max_height = height;
1084 self.options.size = crate::console::ConsoleDimensions { width, height };
1085 }
1086
1087 pub fn on_broken_pipe(&self) {
1095 }
1099}
1100
1101impl Default for Console {
1102 fn default() -> Self {
1103 Self::new()
1104 }
1105}
1106
1107impl fmt::Debug for Console {
1108 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1109 f.debug_struct("Console")
1110 .field("color_system", &self.color_system)
1111 .field("width", &self.width())
1112 .field("height", &self.height())
1113 .field("is_terminal", &self.is_terminal)
1114 .field("alt_screen", &self.alt_screen)
1115 .field("cursor_visible", &self.cursor_visible)
1116 .field("quiet", &self.quiet)
1117 .field("soft_wrap", &self.soft_wrap)
1118 .finish()
1119 }
1120}
1121
1122impl Console {
1127 pub fn begin_capture(&mut self) {
1133 let buf = Arc::new(Mutex::new(Vec::new()));
1134 let writer = Box::new(CaptureWriter { buf: buf.clone() });
1135 self.saved_file = Some(std::mem::replace(&mut self.file, writer));
1136 self.capture_buf = Some(buf);
1137 }
1138
1139 pub fn end_capture(&mut self) -> Capture {
1143 let buf = self.capture_buf.take().expect("not currently capturing");
1144 if let Some(saved) = self.saved_file.take() {
1145 self.file = saved;
1146 }
1147 Capture { buf }
1148 }
1149
1150 pub fn capture<F: FnOnce(&mut Self)>(&mut self, f: F) -> String {
1163 self.begin_capture();
1164 f(self);
1165 let cap = self.end_capture();
1166 cap.get()
1167 }
1168
1169 pub fn pager(&mut self, styles: bool) -> PagerContext {
1177 PagerContext::new(Pager::new().color(styles))
1178 }
1179
1180 pub fn input_renderable(&mut self, prompt: &dyn Renderable) -> String {
1186 if !self.quiet {
1187 let result = prompt.render(&self.options);
1188 let ansi = result.to_ansi();
1189 let _ = write!(self.file, "{ansi}");
1190 let _ = self.file.flush();
1191 }
1192 let mut input = String::new();
1193 let _ = io::stdin().read_line(&mut input);
1194 input.trim().to_string()
1195 }
1196
1197 pub fn print_exception(&mut self, _width: Option<usize>, _extra_lines: usize) {
1206 if self.quiet { return; }
1207 let msg = format!(
1212 "[bold red]Exception[/bold red]: No current exception info. "
1213 );
1214 let msg_text = crate::text::Text::from_markup(&msg);
1215 let result = msg_text.render();
1216 let _ = writeln!(self.file, "{result}");
1217 let _ = self.file.flush();
1218 }
1219
1220 pub fn print_json_str(&mut self, json: &str) {
1225 if self.quiet { return; }
1226 if let Ok(value) = serde_json::from_str::<serde_json::Value>(json) {
1227 self.print_json(&value);
1228 } else {
1229 let _ = writeln!(self.file, "[invalid JSON]");
1230 let _ = self.file.flush();
1231 }
1232 }
1233
1234 pub fn render_to_lines(
1242 &self,
1243 renderable: &dyn Renderable,
1244 options: &ConsoleOptions,
1245 ) -> Vec<Vec<Segment>> {
1246 let result = renderable.render(options);
1247 let has_items = !result.items.is_empty();
1248 let mut lines = if result.lines.is_empty() && has_items {
1249 let flat = result.flatten(options);
1250 if flat.is_empty() {
1251 Vec::new() } else {
1253 vec![flat]
1254 }
1255 } else {
1256 result.lines
1257 };
1258 if !self.render_hooks.is_empty() {
1260 for hook in &self.render_hooks {
1261 lines = hook.apply(&lines);
1262 }
1263 }
1264 lines
1265 }
1266
1267 pub fn render_ansi(&self, text: &str) -> String {
1272 let t = self.render_str(text, "");
1273 t.render()
1274 }
1275
1276 pub fn export_svg_opts(&self, options: &crate::export::ExportSvgOptions) -> String {
1283 crate::export::export_svg(options)
1284 }
1285
1286 pub fn size(&self) -> ConsoleDimensions {
1290 ConsoleDimensions {
1291 width: self.width(),
1292 height: self.height(),
1293 }
1294 }
1295
1296 pub fn is_dumb_terminal(&self) -> bool {
1298 std::env::var("TERM").map_or(false, |t| t == "dumb")
1299 }
1300
1301 pub fn is_alt_screen(&self) -> bool {
1303 self.alt_screen
1304 }
1305
1306 pub fn set_cursor_visible(&mut self, visible: bool) {
1313 self.cursor_visible = visible;
1314 if visible {
1315 let _ = write!(self.file, "\x1b[?25h");
1316 } else {
1317 let _ = write!(self.file, "\x1b[?25l");
1318 }
1319 let _ = self.file.flush();
1320 }
1321
1322 pub fn use_theme(&mut self, theme: Theme) -> ThemeContext {
1338 let prev = std::mem::replace(&mut self.theme, theme);
1339 ThemeContext::new(self, prev)
1340 }
1341
1342 pub fn clear_live(&mut self) {
1346 if self.alt_screen {
1347 let _ = write!(self.file, "\x1b[2J\x1b[H");
1348 } else {
1349 let _ = write!(self.file, "\x1b[2J\x1b[H");
1350 }
1351 let _ = self.file.flush();
1352 }
1353
1354 pub fn set_live(&mut self, _live: &crate::live::Live) {
1360 }
1363
1364 pub fn update_screen(&mut self, renderable: &dyn Renderable, options: Option<&ConsoleOptions>) {
1369 let opts = options.unwrap_or(&self.options);
1370 let segments = self.render(renderable, opts);
1371 let mut output = String::new();
1372 for seg in &segments {
1373 output.push_str(&seg.to_ansi());
1374 }
1375 let _ = write!(self.file, "\x1b[2J\x1b[H{output}");
1376 let _ = self.file.flush();
1377 }
1378
1379 pub fn update_screen_lines(&mut self, lines: &[Vec<Segment>], options: Option<&ConsoleOptions>) {
1384 let _ = options;
1385 let mut output = String::new();
1386 for line in lines {
1387 for seg in line {
1388 output.push_str(&seg.to_ansi());
1389 }
1390 output.push('\n');
1391 }
1392 let _ = write!(self.file, "\x1b[2J\x1b[H{output}");
1393 let _ = self.file.flush();
1394 }
1395
1396 pub fn push_render_hook(&mut self, hook: RenderHook) {
1401 self.render_hooks.push(hook);
1402 }
1403
1404 pub fn pop_render_hook(&mut self) -> Option<RenderHook> {
1406 self.render_hooks.pop()
1407 }
1408}
1409
1410fn detect_color_system() -> ColorSystem {
1419 if let Ok(val) = std::env::var("COLORTERM") {
1421 if val == "truecolor" || val == "24bit" {
1422 return ColorSystem::TrueColor;
1423 }
1424 }
1425 if let Ok(term) = std::env::var("TERM") {
1426 if term.contains("256color") {
1427 return ColorSystem::EightBit;
1428 }
1429 if term == "xterm-kitty" {
1430 return ColorSystem::TrueColor;
1431 }
1432 }
1433 if std::env::var("NO_COLOR").is_ok() {
1435 return ColorSystem::Standard;
1436 }
1437 if atty::is(atty::Stream::Stdout) {
1439 ColorSystem::TrueColor
1440 } else {
1441 ColorSystem::Standard
1442 }
1443}
1444
1445use once_cell::sync::Lazy;
1450
1451static GLOBAL_CONSOLE: Lazy<Mutex<Console>> = Lazy::new(|| {
1452 Mutex::new(Console::new())
1453});
1454
1455pub fn get_console() -> std::sync::MutexGuard<'static, Console> {
1457 GLOBAL_CONSOLE.lock().unwrap()
1458}
1459
1460pub fn print_objects(objects: &[&dyn Renderable]) {
1466 let mut console = GLOBAL_CONSOLE.lock().unwrap();
1467 console.print(objects, " ", "\n");
1468}
1469
1470pub fn print_str(text: &str) {
1472 let mut console = GLOBAL_CONSOLE.lock().unwrap();
1473 console.print_str(text);
1474}
1475
1476pub fn print_json_val(data: &serde_json::Value) {
1478 let mut console = GLOBAL_CONSOLE.lock().unwrap();
1479 console.print_json(data);
1480}
1481
1482pub fn reconfigure(
1496 width: Option<usize>,
1497 height: Option<usize>,
1498 color_system: Option<ColorSystem>,
1499) {
1500 let mut console = GLOBAL_CONSOLE.lock().unwrap();
1501 if let Some(w) = width {
1502 console.set_width(w);
1503 }
1504 if let Some(h) = height {
1505 console.set_height(h);
1506 }
1507 if let Some(cs) = color_system {
1508 console.color_system = cs;
1509 }
1510}
1511
1512#[cfg(test)]
1513mod tests {
1514 use super::*;
1515
1516 #[test]
1517 fn test_render_result_from_text() {
1518 let r = RenderResult::from_text("hello");
1519 assert_eq!(r.lines.len(), 1);
1520 assert_eq!(r.lines[0][0].text, "hello");
1521 }
1522
1523 #[test]
1524 fn test_console_options_default() {
1525 let opts = ConsoleOptions::default();
1526 assert!(opts.markup);
1527 }
1528
1529 #[test]
1530 fn test_console_quiet_default() {
1531 let console = Console::new();
1532 assert!(!console.quiet);
1533 }
1534
1535 #[test]
1536 fn test_console_quiet_setter() {
1537 let mut console = Console::new();
1538 console.set_quiet(true);
1539 assert!(console.quiet);
1540 }
1541
1542 #[test]
1543 fn test_console_quiet_builder() {
1544 let console = Console::new().quiet(true);
1545 assert!(console.quiet);
1546 }
1547
1548 #[test]
1549 fn test_console_quiet_suppresses_print() {
1550 let mut console = Console::new();
1551 console.quiet = true;
1552 console.print(&[], " ", "\n");
1554 console.println(&"test");
1555 console.print_str("test");
1556 }
1557
1558 #[test]
1559 fn test_console_soft_wrap_default() {
1560 let console = Console::new();
1561 assert!(!console.soft_wrap);
1562 }
1563
1564 #[test]
1565 fn test_console_soft_wrap_setter() {
1566 let mut console = Console::new();
1567 console.set_soft_wrap(true);
1568 assert!(console.soft_wrap);
1569 }
1570
1571 #[test]
1572 fn test_console_soft_wrap_builder() {
1573 let console = Console::new().soft_wrap(true);
1574 assert!(console.soft_wrap);
1575 }
1576
1577 #[test]
1578 fn test_console_is_terminal() {
1579 let console = Console::new();
1580 let detected = console.is_terminal();
1582 assert_eq!(detected, atty::is(atty::Stream::Stdout));
1583 }
1584
1585 #[test]
1586 fn test_console_set_size() {
1587 let mut console = Console::new();
1588 console.set_size(120, 30);
1589 assert_eq!(console.width(), 120);
1590 assert_eq!(console.height(), 30);
1591 assert_eq!(console.options.max_width, 120);
1592 assert_eq!(console.options.max_height, 30);
1593 }
1594
1595 #[test]
1596 fn test_console_set_alt_screen() {
1597 let mut console = Console::new();
1598 console.set_alt_screen(true);
1600 console.set_alt_screen(false);
1601 }
1602
1603 #[test]
1604 fn test_console_on_broken_pipe() {
1605 let console = Console::new();
1606 console.on_broken_pipe(); }
1608
1609 #[test]
1610 fn test_console_input_normal() {
1611 let _console = Console::new();
1614 }
1616
1617 #[test]
1618 fn test_console_debug() {
1619 let console = Console::new();
1620 let debug = format!("{:?}", console);
1621 assert!(debug.contains("Console"));
1622 }
1623
1624 #[test]
1625 fn test_console_with_file_has_no_terminal() {
1626 let console = Console::with_file(Box::new(std::io::sink()));
1627 assert!(!console.is_terminal());
1628 }
1629
1630 #[test]
1633 fn test_newline_renderable() {
1634 let nl = NewLine;
1635 let result = nl.render(&ConsoleOptions::default());
1636 let ansi = result.to_ansi();
1637 assert_eq!(ansi, "\n");
1638 }
1639
1640 #[test]
1641 fn test_nochange_renderable() {
1642 let nc = NoChange;
1643 let result = nc.render(&ConsoleOptions::default());
1644 assert!(result.lines.is_empty());
1645 assert!(result.items.is_empty());
1646 }
1647
1648 #[test]
1649 fn test_capture_begin_end() {
1650 let mut console = Console::with_file(Box::new(std::io::sink()));
1651 console.begin_capture();
1652 let _ = write!(console.file, "captured text");
1653 let cap = console.end_capture();
1654 assert_eq!(cap.get(), "captured text");
1655 }
1656
1657 #[test]
1658 fn test_capture_with_closure() {
1659 let mut console = Console::with_file(Box::new(std::io::sink()));
1660 let output = console.capture(|c| {
1661 let _ = write!(c.file, "hello from capture");
1662 });
1663 assert_eq!(output, "hello from capture");
1664 }
1665
1666 #[test]
1667 fn test_capture_new_empty() {
1668 let console = Console::new();
1669 let cap = Capture::new(&console);
1670 assert_eq!(cap.get(), "");
1671 }
1672
1673 #[test]
1674 fn test_system_pager_default() {
1675 let pager = SystemPager::new();
1676 let _ = pager.show("");
1679 }
1680
1681 #[test]
1682 fn test_pager_enabled() {
1683 let pager = Pager::new();
1684 assert!(pager.is_enabled());
1685 let disabled = pager.enabled(false);
1686 assert!(!disabled.is_enabled());
1687 }
1688
1689 #[test]
1690 fn test_render_hook() {
1691 let hook = RenderHook::new(|lines| {
1692 let hooked: Vec<Vec<Segment>> = lines.iter().map(|line| {
1694 let mut new_line = line.clone();
1695 new_line.push(Segment::styled("HOOKED", Style::new().bold(true)));
1696 new_line
1697 }).collect();
1698 hooked
1699 });
1700 let lines = vec![vec![Segment::new("test")]];
1701 let result = hook.apply(&lines);
1702 assert_eq!(result.len(), 1);
1703 assert_eq!(result[0].len(), 2);
1704 assert_eq!(result[0][1].text, "HOOKED");
1705 }
1706
1707 #[test]
1708 fn test_console_size() {
1709 let mut console = Console::new();
1710 console.set_size(100, 40);
1711 let dims = console.size();
1712 assert_eq!(dims.width, 100);
1713 assert_eq!(dims.height, 40);
1714 }
1715
1716 #[test]
1717 fn test_console_is_dumb_terminal() {
1718 let console = Console::new();
1719 let _ = console.is_dumb_terminal();
1722 }
1723
1724 #[test]
1725 fn test_console_is_alt_screen() {
1726 let mut console = Console::new();
1727 assert!(!console.is_alt_screen());
1728 console.alt_screen = true;
1729 assert!(console.is_alt_screen());
1730 }
1731
1732 #[test]
1733 fn test_console_render_ansi() {
1734 let console = Console::new();
1735 let ansi = console.render_ansi("test");
1736 assert!(ansi.contains("test") || ansi.contains("\x1b["));
1738 }
1739
1740 #[test]
1741 fn test_console_render_to_lines() {
1742 let console = Console::new();
1743 let opts = ConsoleOptions::default();
1744 let lines = console.render_to_lines(&"hello", &opts);
1745 assert_eq!(lines.len(), 1);
1746 assert_eq!(lines[0][0].text, "hello");
1747 }
1748
1749 #[test]
1750 fn test_console_input_renderable() {
1751 let _console = Console::new();
1754 }
1755
1756 #[test]
1757 fn test_console_print_exception_noop() {
1758 let mut console = Console::new();
1759 console.print_exception(None, 3);
1761 }
1762
1763 #[test]
1764 fn test_console_render_hooks_push_pop() {
1765 let mut console = Console::new();
1766 let hook = RenderHook::new(|lines| lines.to_vec());
1767 console.push_render_hook(hook);
1768 assert_eq!(console.render_hooks.len(), 1);
1769 let popped = console.pop_render_hook();
1770 assert!(popped.is_some());
1771 assert!(console.render_hooks.is_empty());
1772 }
1773
1774 #[test]
1775 fn test_console_reconfigure() {
1776 reconfigure(Some(120), Some(40), None);
1778 reconfigure(None, None, Some(ColorSystem::Standard));
1779 reconfigure(None, None, None);
1781 }
1782
1783 #[test]
1784 fn test_pager_context_write() {
1785 let pager = Pager::new().enabled(false);
1786 let mut ctx = PagerContext::new(pager);
1787 ctx.feed("test content");
1788 }
1790
1791 #[test]
1792 fn test_theme_context() {
1793 let mut console = Console::new();
1794 let custom_theme = Theme::new();
1795 let original = console.theme.clone();
1796 {
1797 let _ctx = console.use_theme(custom_theme);
1798 }
1800 assert_eq!(console.theme.styles.len(), original.styles.len());
1802 }
1803}