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<'a> {
522 _phantom: std::marker::PhantomData<&'a mut Console>,
523 console_ptr: *mut Console,
524 previous_theme: Theme,
525}
526
527impl<'a> ThemeContext<'a> {
532 pub(crate) fn new(console: &'a mut Console, previous_theme: Theme) -> Self {
534 Self {
535 _phantom: std::marker::PhantomData,
536 console_ptr: console as *mut Console,
537 previous_theme,
538 }
539 }
540}
541
542impl<'a> Drop for ThemeContext<'a> {
543 fn drop(&mut self) {
544 unsafe {
545 (*self.console_ptr).theme = std::mem::take(&mut self.previous_theme);
546 }
547 }
548}
549
550pub struct Console {
556 pub file: Box<dyn Write + Send>,
558 pub color_system: ColorSystem,
560 pub theme: Theme,
562 pub options: ConsoleOptions,
564 width: Option<usize>,
566 height: Option<usize>,
568 is_terminal: bool,
570 pub quiet: bool,
572 pub soft_wrap: bool,
574 alt_screen: bool,
576 cursor_visible: bool,
578 render_hooks: Vec<RenderHook>,
580 capture_buf: Option<Arc<Mutex<Vec<u8>>>>,
582 saved_file: Option<Box<dyn Write + Send>>,
584}
585
586impl Console {
587 pub fn new() -> Self {
589 let is_terminal = atty::is(atty::Stream::Stdout);
590 let color_system = detect_color_system();
591
592 let size = ConsoleDimensions::detect();
593 let render_width = size.width.saturating_sub(1);
597
598 Self {
599 file: Box::new(io::stdout()) as Box<dyn Write + Send>,
600 color_system,
601 theme: crate::theme::default_theme(),
602 options: ConsoleOptions {
603 size,
604 is_terminal,
605 max_width: render_width,
606 max_height: size.height,
607 ..Default::default()
608 },
609 width: None,
610 height: None,
611 is_terminal,
612 quiet: false,
613 soft_wrap: false,
614 alt_screen: false,
615 cursor_visible: true,
616 render_hooks: Vec::new(),
617 capture_buf: None,
618 saved_file: None,
619 }
620 }
621
622 pub fn with_file(file: Box<dyn Write + Send>) -> Self {
624 let _is_terminal = false;
625 Self {
626 file,
627 color_system: ColorSystem::Standard,
628 theme: crate::theme::default_theme(),
629 options: ConsoleOptions {
630 size: ConsoleDimensions { width: 80, height: 25 },
631 is_terminal: false,
632 max_width: 80,
633 max_height: 25,
634 ..Default::default()
635 },
636 width: None,
637 height: None,
638 is_terminal: false,
639 quiet: false,
640 soft_wrap: false,
641 alt_screen: false,
642 cursor_visible: true,
643 render_hooks: Vec::new(),
644 capture_buf: None,
645 saved_file: None,
646 }
647 }
648
649 pub fn set_width(&mut self, width: usize) {
651 self.width = Some(width);
652 self.options.max_width = width;
653 }
654
655 pub fn set_height(&mut self, height: usize) {
657 self.height = Some(height);
658 self.options.max_height = height;
659 }
660
661 pub fn width(&self) -> usize {
663 self.width.unwrap_or(self.options.size.width)
664 }
665
666 pub fn height(&self) -> usize {
668 self.height.unwrap_or(self.options.size.height)
669 }
670
671 pub fn render_lines(
673 &self,
674 renderable: &dyn Renderable,
675 options: &ConsoleOptions,
676 style: Option<&Style>,
677 _pad: bool,
678 ) -> Vec<Vec<Segment>> {
679 let result = renderable.render(options);
680
681 if let Some(st) = style {
682 result
683 .lines
684 .into_iter()
685 .map(|line| {
686 line.into_iter()
687 .map(|seg| {
688 let new_style = if let Some(ref s) = seg.style {
689 s.combine(st)
690 } else {
691 st.clone()
692 };
693 Segment::styled(seg.text, new_style)
694 })
695 .collect()
696 })
697 .collect()
698 } else {
699 result.lines
700 }
701 }
702
703 pub fn get_style(&self, name: &str, default: &str) -> Option<Style> {
705 self.theme
706 .get(name)
707 .cloned()
708 .or_else(|| {
709 if !default.is_empty() {
710 Some(Style::from_str(default))
711 } else {
712 None
713 }
714 })
715 }
716
717 pub fn render_str(&self, text: &str, style: &str) -> Text {
719 let st = self.get_style(style, "");
720 let mut t = Text::new(text);
721 if let Some(s) = st {
722 t = t.style(s);
723 }
724 t
725 }
726
727 pub fn print(&mut self, objects: &[&dyn Renderable], sep: &str, end: &str) {
734 if self.quiet { return; }
735 let mut first = true;
736 for obj in objects {
737 if !first {
738 let _ = write!(self.file, "{sep}");
739 }
740 first = false;
741 let result = obj.render(&self.options);
742 let ansi = result.to_ansi();
743 let _ = write!(self.file, "{ansi}");
744 }
745 let _ = write!(self.file, "{end}");
746 let _ = self.file.flush();
747 }
748
749 pub fn println(&mut self, renderable: &dyn Renderable) {
754 if self.quiet { return; }
755 self.refresh_size();
756 let result = renderable.render(&self.options);
757 let ansi = result.to_ansi();
758 let _ = writeln!(self.file, "{ansi}");
759 let _ = self.file.flush();
760 }
761
762 fn refresh_size(&mut self) {
764 if self.is_terminal {
765 let size = ConsoleDimensions::detect();
766 self.options.size = size;
767 self.options.max_width = size.width.saturating_sub(1);
769 self.options.max_height = size.height;
770 }
771 }
772
773 pub fn print_str(&mut self, text: &str) {
776 if self.quiet { return; }
777 let ansi = if self.options.markup {
778 let parsed = crate::markup::render(text);
779 parsed.render()
780 } else {
781 text.to_string()
782 };
783 let _ = write!(self.file, "{ansi}");
784 let _ = self.file.flush();
785 }
786
787 pub fn print_json(&mut self, data: &serde_json::Value) {
789 if self.quiet { return; }
790 let formatted = crate::json::render_json(data);
791 let result = formatted.render(&self.options);
792 let ansi = result.to_ansi();
793 let _ = writeln!(self.file, "{ansi}");
794 let _ = self.file.flush();
795 }
796
797 pub fn clear(&mut self) {
799 if self.quiet { return; }
800 let _ = write!(self.file, "\x1b[2J\x1b[H");
801 let _ = self.file.flush();
802 }
803
804 pub fn show_cursor(&mut self) {
806 self.cursor_visible = true;
807 let _ = write!(self.file, "\x1b[?25h");
808 let _ = self.file.flush();
809 }
810
811 pub fn hide_cursor(&mut self) {
813 self.cursor_visible = false;
814 let _ = write!(self.file, "\x1b[?25l");
815 let _ = self.file.flush();
816 }
817
818 pub fn set_window_title(&mut self, title: &str) {
820 let _ = write!(self.file, "\x1b]0;{title}\x07");
821 let _ = self.file.flush();
822 }
823
824 pub fn color_ansi(&self, color: &Color) -> String {
826 let downgraded = color.downgrade(self.color_system);
827 downgraded.to_string()
828 }
829
830 pub fn render(&self, renderable: &dyn Renderable, options: &ConsoleOptions) -> Vec<Segment> {
837 let result = renderable.render(options);
838 result.flatten(options)
839 }
840
841 pub fn measure(&self, renderable: &dyn Renderable, options: &ConsoleOptions) -> crate::measure::Measurement {
844 if let Some(m) = renderable.measure(options) {
845 return m;
846 }
847 let segments = self.render(renderable, options);
848 let max_w = segments.iter()
849 .map(|s| s.cell_length())
850 .max()
851 .unwrap_or(0);
852 crate::measure::Measurement::new(max_w, options.max_width)
853 }
854
855 pub fn rule(
860 &mut self,
861 title: impl Into<String>,
862 characters: Option<&str>,
863 style: Option<Style>,
864 align: Option<AlignMethod>,
865 ) {
866 if self.quiet { return; }
867 let mut rule = crate::rule::Rule::new().title(title);
868 if let Some(chars) = characters { rule = rule.characters(chars); }
869 if let Some(st) = style { rule = rule.style(st); }
870 if let Some(a) = align { rule = rule.align(a); }
871 let result = rule.render(&self.options);
872 let ansi = result.to_ansi();
873 let _ = write!(self.file, "{ansi}");
874 let _ = self.file.flush();
875 }
876
877 pub fn bell(&mut self) {
879 if self.quiet { return; }
880 let _ = write!(self.file, "\x07");
881 let _ = self.file.flush();
882 }
883
884 pub fn line(&mut self, count: usize) {
886 if self.quiet { return; }
887 for _ in 0..count {
888 let _ = writeln!(self.file);
889 }
890 let _ = self.file.flush();
891 }
892
893 pub fn log(&mut self, objects: &[&dyn Renderable]) {
895 if self.quiet { return; }
896 let now = chrono::Local::now();
897 let time_str = format!("[{}]", now.format("%H:%M:%S"));
898 let _ = write!(self.file, "{} ", Style::new().dim(true).to_ansi());
899 let _ = write!(self.file, "{time_str} ");
900 let _ = write!(self.file, "{}", Style::new().reset_ansi());
901 self.print(objects, " ", "\n");
902 }
903
904 pub fn push_theme(&mut self, theme: Theme) {
908 let mut new_theme = theme.clone();
909 new_theme.inherit = Some(Box::new(self.theme.clone()));
910 self.theme = new_theme;
911 }
912
913 pub fn pop_theme(&mut self) {
915 if let Some(ref inherit) = self.theme.inherit {
916 self.theme = *inherit.clone();
917 }
918 }
919
920 pub fn export_html(&self, renderable: &dyn Renderable) -> String {
926 let result = renderable.render(&self.options);
927 let ansi = result.to_ansi();
928 crate::export::export_html(&crate::export::ExportHtmlOptions {
929 code: crate::export::strip_ansi_escapes(&ansi),
930 ..Default::default()
931 })
932 }
933
934 pub fn save_html(&self, path: impl AsRef<std::path::Path>, renderable: &dyn Renderable) -> std::io::Result<()> {
936 let html = self.export_html(renderable);
937 crate::export::save_html(path, &crate::export::ExportHtmlOptions {
938 code: html,
939 ..Default::default()
940 })
941 }
942
943 pub fn export_svg(&self, renderable: &dyn Renderable) -> String {
945 let result = renderable.render(&self.options);
946 let ansi = result.to_ansi();
947 crate::export::export_svg(&crate::export::ExportSvgOptions {
948 code: crate::export::strip_ansi_escapes(&ansi),
949 ..Default::default()
950 })
951 }
952
953 pub fn save_svg(&self, path: impl AsRef<std::path::Path>, renderable: &dyn Renderable) -> std::io::Result<()> {
955 let svg = self.export_svg(renderable);
956 crate::export::save_svg(path, &crate::export::ExportSvgOptions {
957 code: svg,
958 ..Default::default()
959 })
960 }
961
962 pub fn export_text(&self, renderable: &dyn Renderable) -> String {
964 let result = renderable.render(&self.options);
965 let ansi = result.to_ansi();
966 crate::export::export_text(&crate::export::ExportTextOptions {
967 text: ansi,
968 strip_ansi: true,
969 })
970 }
971
972 pub fn save_text(&self, path: impl AsRef<std::path::Path>, renderable: &dyn Renderable) -> std::io::Result<()> {
974 let text = self.export_text(renderable);
975 crate::export::save_text(path, &crate::export::ExportTextOptions {
976 text,
977 strip_ansi: false,
978 })
979 }
980
981 pub fn set_quiet(&mut self, quiet: bool) {
985 self.quiet = quiet;
986 }
987
988 pub fn quiet(mut self, quiet: bool) -> Self {
990 self.quiet = quiet;
991 self
992 }
993
994 pub fn set_soft_wrap(&mut self, soft_wrap: bool) {
996 self.soft_wrap = soft_wrap;
997 }
998
999 pub fn soft_wrap(mut self, soft_wrap: bool) -> Self {
1001 self.soft_wrap = soft_wrap;
1002 self
1003 }
1004
1005 pub fn input(&mut self, prompt: &str, password: bool) -> String {
1013 let _ = write!(self.file, "{prompt}");
1014 let _ = self.file.flush();
1015
1016 if password {
1017 self.read_password()
1018 } else {
1019 let mut input = String::new();
1020 let _ = io::stdin().read_line(&mut input);
1021 input.trim().to_string()
1022 }
1023 }
1024
1025 fn read_password(&mut self) -> String {
1027 use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
1028 use std::io::Read;
1029
1030 match enable_raw_mode() {
1031 Ok(()) => {
1032 let stdin = io::stdin();
1033 let mut handle = stdin.lock();
1034 let mut buf = [0u8; 1];
1035 let mut password = String::new();
1036
1037 loop {
1038 match handle.read_exact(&mut buf) {
1039 Ok(()) => match buf[0] {
1040 b'\r' | b'\n' => {
1041 let _ = writeln!(self.file);
1042 let _ = self.file.flush();
1043 break;
1044 }
1045 b'\x03' => {
1046 let _ = writeln!(self.file);
1048 let _ = self.file.flush();
1049 break;
1050 }
1051 b'\x7f' | b'\x08' => {
1052 password.pop();
1054 }
1055 c => {
1056 password.push(c as char);
1057 let _ = write!(self.file, "*");
1058 let _ = self.file.flush();
1059 }
1060 },
1061 Err(_) => break,
1062 }
1063 }
1064 let _ = disable_raw_mode();
1065 password
1066 }
1067 Err(_) => {
1068 let mut input = String::new();
1070 let _ = io::stdin().read_line(&mut input);
1071 input.trim().to_string()
1072 }
1073 }
1074 }
1075
1076 pub fn screen(&mut self) -> crate::screen::ScreenContext {
1082 let mut ctx = crate::screen::ScreenContext::new();
1083 ctx.enter();
1084 ctx
1085 }
1086
1087 pub fn set_alt_screen(&mut self, enable: bool) {
1090 self.alt_screen = enable;
1091 if enable {
1092 let _ = write!(self.file, "\x1b[?1049h");
1093 } else {
1094 let _ = write!(self.file, "\x1b[?1049l");
1095 }
1096 let _ = self.file.flush();
1097 }
1098
1099 pub fn is_terminal(&self) -> bool {
1101 self.is_terminal
1102 }
1103
1104 pub fn set_size(&mut self, width: usize, height: usize) {
1106 self.width = Some(width);
1107 self.height = Some(height);
1108 self.options.max_width = width;
1109 self.options.max_height = height;
1110 self.options.size = crate::console::ConsoleDimensions { width, height };
1111 }
1112
1113 pub fn on_broken_pipe(&self) {
1121 }
1125}
1126
1127impl Default for Console {
1128 fn default() -> Self {
1129 Self::new()
1130 }
1131}
1132
1133impl fmt::Debug for Console {
1134 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1135 f.debug_struct("Console")
1136 .field("color_system", &self.color_system)
1137 .field("width", &self.width())
1138 .field("height", &self.height())
1139 .field("is_terminal", &self.is_terminal)
1140 .field("alt_screen", &self.alt_screen)
1141 .field("cursor_visible", &self.cursor_visible)
1142 .field("quiet", &self.quiet)
1143 .field("soft_wrap", &self.soft_wrap)
1144 .finish()
1145 }
1146}
1147
1148impl Console {
1153 pub fn begin_capture(&mut self) {
1159 let buf = Arc::new(Mutex::new(Vec::new()));
1160 let writer = Box::new(CaptureWriter { buf: buf.clone() });
1161 self.saved_file = Some(std::mem::replace(&mut self.file, writer));
1162 self.capture_buf = Some(buf);
1163 }
1164
1165 pub fn end_capture(&mut self) -> Capture {
1169 let buf = self.capture_buf.take().expect("not currently capturing");
1170 if let Some(saved) = self.saved_file.take() {
1171 self.file = saved;
1172 }
1173 Capture { buf }
1174 }
1175
1176 pub fn capture<F: FnOnce(&mut Self)>(&mut self, f: F) -> String {
1189 self.begin_capture();
1190 f(self);
1191 let cap = self.end_capture();
1192 cap.get()
1193 }
1194
1195 pub fn pager(&mut self, styles: bool) -> PagerContext {
1203 PagerContext::new(Pager::new().color(styles))
1204 }
1205
1206 pub fn input_renderable(&mut self, prompt: &dyn Renderable) -> String {
1212 if !self.quiet {
1213 let result = prompt.render(&self.options);
1214 let ansi = result.to_ansi();
1215 let _ = write!(self.file, "{ansi}");
1216 let _ = self.file.flush();
1217 }
1218 let mut input = String::new();
1219 let _ = io::stdin().read_line(&mut input);
1220 input.trim().to_string()
1221 }
1222
1223 pub fn print_exception(&mut self, _width: Option<usize>, _extra_lines: usize) {
1232 if self.quiet { return; }
1233 let msg = format!(
1238 "[bold red]Exception[/bold red]: No current exception info. "
1239 );
1240 let msg_text = crate::text::Text::from_markup(&msg);
1241 let result = msg_text.render();
1242 let _ = writeln!(self.file, "{result}");
1243 let _ = self.file.flush();
1244 }
1245
1246 pub fn print_json_str(&mut self, json: &str) {
1251 if self.quiet { return; }
1252 if let Ok(value) = serde_json::from_str::<serde_json::Value>(json) {
1253 self.print_json(&value);
1254 } else {
1255 let _ = writeln!(self.file, "[invalid JSON]");
1256 let _ = self.file.flush();
1257 }
1258 }
1259
1260 pub fn render_to_lines(
1268 &self,
1269 renderable: &dyn Renderable,
1270 options: &ConsoleOptions,
1271 ) -> Vec<Vec<Segment>> {
1272 let result = renderable.render(options);
1273 let has_items = !result.items.is_empty();
1274 let mut lines = if result.lines.is_empty() && has_items {
1275 let flat = result.flatten(options);
1276 if flat.is_empty() {
1277 Vec::new() } else {
1279 vec![flat]
1280 }
1281 } else {
1282 result.lines
1283 };
1284 if !self.render_hooks.is_empty() {
1286 for hook in &self.render_hooks {
1287 lines = hook.apply(&lines);
1288 }
1289 }
1290 lines
1291 }
1292
1293 pub fn render_ansi(&self, text: &str) -> String {
1298 let t = self.render_str(text, "");
1299 t.render()
1300 }
1301
1302 pub fn export_svg_opts(&self, options: &crate::export::ExportSvgOptions) -> String {
1309 crate::export::export_svg(options)
1310 }
1311
1312 pub fn size(&self) -> ConsoleDimensions {
1316 ConsoleDimensions {
1317 width: self.width(),
1318 height: self.height(),
1319 }
1320 }
1321
1322 pub fn is_dumb_terminal(&self) -> bool {
1324 std::env::var("TERM").map_or(false, |t| t == "dumb")
1325 }
1326
1327 pub fn is_alt_screen(&self) -> bool {
1329 self.alt_screen
1330 }
1331
1332 pub fn set_cursor_visible(&mut self, visible: bool) {
1339 self.cursor_visible = visible;
1340 if visible {
1341 let _ = write!(self.file, "\x1b[?25h");
1342 } else {
1343 let _ = write!(self.file, "\x1b[?25l");
1344 }
1345 let _ = self.file.flush();
1346 }
1347
1348 pub fn use_theme(&mut self, theme: Theme) -> ThemeContext<'_> {
1364 let prev = std::mem::replace(&mut self.theme, theme);
1365 ThemeContext::new(self, prev)
1366 }
1367
1368 pub fn clear_live(&mut self) {
1372 if self.alt_screen {
1373 let _ = write!(self.file, "\x1b[2J\x1b[H");
1374 } else {
1375 let _ = write!(self.file, "\x1b[2J\x1b[H");
1376 }
1377 let _ = self.file.flush();
1378 }
1379
1380 pub fn set_live(&mut self, _live: &crate::live::Live) {
1386 }
1389
1390 pub fn update_screen(&mut self, renderable: &dyn Renderable, options: Option<&ConsoleOptions>) {
1395 let opts = options.unwrap_or(&self.options);
1396 let segments = self.render(renderable, opts);
1397 let mut output = String::new();
1398 for seg in &segments {
1399 output.push_str(&seg.to_ansi());
1400 }
1401 let _ = write!(self.file, "\x1b[2J\x1b[H{output}");
1402 let _ = self.file.flush();
1403 }
1404
1405 pub fn update_screen_lines(&mut self, lines: &[Vec<Segment>], options: Option<&ConsoleOptions>) {
1410 let _ = options;
1411 let mut output = String::new();
1412 for line in lines {
1413 for seg in line {
1414 output.push_str(&seg.to_ansi());
1415 }
1416 output.push('\n');
1417 }
1418 let _ = write!(self.file, "\x1b[2J\x1b[H{output}");
1419 let _ = self.file.flush();
1420 }
1421
1422 pub fn push_render_hook(&mut self, hook: RenderHook) {
1427 self.render_hooks.push(hook);
1428 }
1429
1430 pub fn pop_render_hook(&mut self) -> Option<RenderHook> {
1432 self.render_hooks.pop()
1433 }
1434}
1435
1436fn detect_color_system() -> ColorSystem {
1445 if let Ok(val) = std::env::var("COLORTERM") {
1447 if val == "truecolor" || val == "24bit" {
1448 return ColorSystem::TrueColor;
1449 }
1450 }
1451 if let Ok(term) = std::env::var("TERM") {
1452 if term.contains("256color") {
1453 return ColorSystem::EightBit;
1454 }
1455 if term == "xterm-kitty" {
1456 return ColorSystem::TrueColor;
1457 }
1458 }
1459 if std::env::var("NO_COLOR").is_ok() {
1461 return ColorSystem::Standard;
1462 }
1463 if atty::is(atty::Stream::Stdout) {
1465 ColorSystem::TrueColor
1466 } else {
1467 ColorSystem::Standard
1468 }
1469}
1470
1471use once_cell::sync::Lazy;
1476
1477static GLOBAL_CONSOLE: Lazy<Mutex<Console>> = Lazy::new(|| {
1478 Mutex::new(Console::new())
1479});
1480
1481pub fn get_console() -> std::sync::MutexGuard<'static, Console> {
1483 GLOBAL_CONSOLE.lock().unwrap()
1484}
1485
1486pub fn print_objects(objects: &[&dyn Renderable]) {
1492 let mut console = GLOBAL_CONSOLE.lock().unwrap();
1493 console.print(objects, " ", "\n");
1494}
1495
1496pub fn print_str(text: &str) {
1498 let mut console = GLOBAL_CONSOLE.lock().unwrap();
1499 console.print_str(text);
1500}
1501
1502pub fn print_json_val(data: &serde_json::Value) {
1504 let mut console = GLOBAL_CONSOLE.lock().unwrap();
1505 console.print_json(data);
1506}
1507
1508pub fn reconfigure(
1522 width: Option<usize>,
1523 height: Option<usize>,
1524 color_system: Option<ColorSystem>,
1525) {
1526 let mut console = GLOBAL_CONSOLE.lock().unwrap();
1527 if let Some(w) = width {
1528 console.set_width(w);
1529 }
1530 if let Some(h) = height {
1531 console.set_height(h);
1532 }
1533 if let Some(cs) = color_system {
1534 console.color_system = cs;
1535 }
1536}
1537
1538#[cfg(test)]
1539mod tests {
1540 use super::*;
1541
1542 #[test]
1543 fn test_render_result_from_text() {
1544 let r = RenderResult::from_text("hello");
1545 assert_eq!(r.lines.len(), 1);
1546 assert_eq!(r.lines[0][0].text, "hello");
1547 }
1548
1549 #[test]
1550 fn test_console_options_default() {
1551 let opts = ConsoleOptions::default();
1552 assert!(opts.markup);
1553 }
1554
1555 #[test]
1556 fn test_console_quiet_default() {
1557 let console = Console::new();
1558 assert!(!console.quiet);
1559 }
1560
1561 #[test]
1562 fn test_console_quiet_setter() {
1563 let mut console = Console::new();
1564 console.set_quiet(true);
1565 assert!(console.quiet);
1566 }
1567
1568 #[test]
1569 fn test_console_quiet_builder() {
1570 let console = Console::new().quiet(true);
1571 assert!(console.quiet);
1572 }
1573
1574 #[test]
1575 fn test_console_quiet_suppresses_print() {
1576 let mut console = Console::new();
1577 console.quiet = true;
1578 console.print(&[], " ", "\n");
1580 console.println(&"test");
1581 console.print_str("test");
1582 }
1583
1584 #[test]
1585 fn test_console_soft_wrap_default() {
1586 let console = Console::new();
1587 assert!(!console.soft_wrap);
1588 }
1589
1590 #[test]
1591 fn test_console_soft_wrap_setter() {
1592 let mut console = Console::new();
1593 console.set_soft_wrap(true);
1594 assert!(console.soft_wrap);
1595 }
1596
1597 #[test]
1598 fn test_console_soft_wrap_builder() {
1599 let console = Console::new().soft_wrap(true);
1600 assert!(console.soft_wrap);
1601 }
1602
1603 #[test]
1604 fn test_console_is_terminal() {
1605 let console = Console::new();
1606 let detected = console.is_terminal();
1608 assert_eq!(detected, atty::is(atty::Stream::Stdout));
1609 }
1610
1611 #[test]
1612 fn test_console_set_size() {
1613 let mut console = Console::new();
1614 console.set_size(120, 30);
1615 assert_eq!(console.width(), 120);
1616 assert_eq!(console.height(), 30);
1617 assert_eq!(console.options.max_width, 120);
1618 assert_eq!(console.options.max_height, 30);
1619 }
1620
1621 #[test]
1622 fn test_console_set_alt_screen() {
1623 let mut console = Console::new();
1624 console.set_alt_screen(true);
1626 console.set_alt_screen(false);
1627 }
1628
1629 #[test]
1630 fn test_console_on_broken_pipe() {
1631 let console = Console::new();
1632 console.on_broken_pipe(); }
1634
1635 #[test]
1636 fn test_console_input_normal() {
1637 let _console = Console::new();
1640 }
1642
1643 #[test]
1644 fn test_console_debug() {
1645 let console = Console::new();
1646 let debug = format!("{:?}", console);
1647 assert!(debug.contains("Console"));
1648 }
1649
1650 #[test]
1651 fn test_console_with_file_has_no_terminal() {
1652 let console = Console::with_file(Box::new(std::io::sink()));
1653 assert!(!console.is_terminal());
1654 }
1655
1656 #[test]
1659 fn test_newline_renderable() {
1660 let nl = NewLine;
1661 let result = nl.render(&ConsoleOptions::default());
1662 let ansi = result.to_ansi();
1663 assert_eq!(ansi, "\n");
1664 }
1665
1666 #[test]
1667 fn test_nochange_renderable() {
1668 let nc = NoChange;
1669 let result = nc.render(&ConsoleOptions::default());
1670 assert!(result.lines.is_empty());
1671 assert!(result.items.is_empty());
1672 }
1673
1674 #[test]
1675 fn test_capture_begin_end() {
1676 let mut console = Console::with_file(Box::new(std::io::sink()));
1677 console.begin_capture();
1678 let _ = write!(console.file, "captured text");
1679 let cap = console.end_capture();
1680 assert_eq!(cap.get(), "captured text");
1681 }
1682
1683 #[test]
1684 fn test_capture_with_closure() {
1685 let mut console = Console::with_file(Box::new(std::io::sink()));
1686 let output = console.capture(|c| {
1687 let _ = write!(c.file, "hello from capture");
1688 });
1689 assert_eq!(output, "hello from capture");
1690 }
1691
1692 #[test]
1693 fn test_capture_new_empty() {
1694 let console = Console::new();
1695 let cap = Capture::new(&console);
1696 assert_eq!(cap.get(), "");
1697 }
1698
1699 #[test]
1700 fn test_system_pager_default() {
1701 let pager = SystemPager::new();
1702 let _ = pager.show("");
1705 }
1706
1707 #[test]
1708 fn test_pager_enabled() {
1709 let pager = Pager::new();
1710 assert!(pager.is_enabled());
1711 let disabled = pager.enabled(false);
1712 assert!(!disabled.is_enabled());
1713 }
1714
1715 #[test]
1716 fn test_render_hook() {
1717 let hook = RenderHook::new(|lines| {
1718 let hooked: Vec<Vec<Segment>> = lines.iter().map(|line| {
1720 let mut new_line = line.clone();
1721 new_line.push(Segment::styled("HOOKED", Style::new().bold(true)));
1722 new_line
1723 }).collect();
1724 hooked
1725 });
1726 let lines = vec![vec![Segment::new("test")]];
1727 let result = hook.apply(&lines);
1728 assert_eq!(result.len(), 1);
1729 assert_eq!(result[0].len(), 2);
1730 assert_eq!(result[0][1].text, "HOOKED");
1731 }
1732
1733 #[test]
1734 fn test_console_size() {
1735 let mut console = Console::new();
1736 console.set_size(100, 40);
1737 let dims = console.size();
1738 assert_eq!(dims.width, 100);
1739 assert_eq!(dims.height, 40);
1740 }
1741
1742 #[test]
1743 fn test_console_is_dumb_terminal() {
1744 let console = Console::new();
1745 let _ = console.is_dumb_terminal();
1748 }
1749
1750 #[test]
1751 fn test_console_is_alt_screen() {
1752 let mut console = Console::new();
1753 assert!(!console.is_alt_screen());
1754 console.alt_screen = true;
1755 assert!(console.is_alt_screen());
1756 }
1757
1758 #[test]
1759 fn test_console_render_ansi() {
1760 let console = Console::new();
1761 let ansi = console.render_ansi("test");
1762 assert!(ansi.contains("test") || ansi.contains("\x1b["));
1764 }
1765
1766 #[test]
1767 fn test_console_render_to_lines() {
1768 let console = Console::new();
1769 let opts = ConsoleOptions::default();
1770 let lines = console.render_to_lines(&"hello", &opts);
1771 assert_eq!(lines.len(), 1);
1772 assert_eq!(lines[0][0].text, "hello");
1773 }
1774
1775 #[test]
1776 fn test_console_input_renderable() {
1777 let _console = Console::new();
1780 }
1781
1782 #[test]
1783 fn test_console_print_exception_noop() {
1784 let mut console = Console::new();
1785 console.print_exception(None, 3);
1787 }
1788
1789 #[test]
1790 fn test_console_render_hooks_push_pop() {
1791 let mut console = Console::new();
1792 let hook = RenderHook::new(|lines| lines.to_vec());
1793 console.push_render_hook(hook);
1794 assert_eq!(console.render_hooks.len(), 1);
1795 let popped = console.pop_render_hook();
1796 assert!(popped.is_some());
1797 assert!(console.render_hooks.is_empty());
1798 }
1799
1800 #[test]
1801 fn test_console_reconfigure() {
1802 reconfigure(Some(120), Some(40), None);
1804 reconfigure(None, None, Some(ColorSystem::Standard));
1805 reconfigure(None, None, None);
1807 }
1808
1809 #[test]
1810 fn test_pager_context_write() {
1811 let pager = Pager::new().enabled(false);
1812 let mut ctx = PagerContext::new(pager);
1813 ctx.feed("test content");
1814 }
1816
1817 #[test]
1818 fn test_theme_context() {
1819 let mut console = Console::new();
1820 let custom_theme = Theme::new();
1821 let original = console.theme.clone();
1822 {
1823 let _ctx = console.use_theme(custom_theme);
1824 }
1826 assert_eq!(console.theme.styles.len(), original.styles.len());
1828 }
1829}