1use std::fmt;
8use std::io::{self, IsTerminal, 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 #[must_use]
124 pub fn update_width(&self, max_width: usize) -> Self {
125 let mut opts = self.clone();
126 opts.max_width = max_width;
127 opts
128 }
129
130 #[must_use]
132 pub fn update_height(&self, height: usize) -> Self {
133 let mut opts = self.clone();
134 opts.height = Some(height);
135 opts
136 }
137
138 #[must_use]
140 pub fn shrink_width(&self, amount: usize) -> Self {
141 let mut opts = self.clone();
142 opts.max_width = opts.max_width.saturating_sub(amount);
143 opts
144 }
145}
146
147#[derive(Clone)]
156pub enum RenderItem {
157 Segment(Segment),
159 Nested(DynRenderable),
161}
162
163impl fmt::Debug for RenderItem {
164 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
165 match self {
166 Self::Segment(s) => write!(f, "Segment({})", &s.text),
167 Self::Nested(_) => write!(f, "Nested(...)"),
168 }
169 }
170}
171
172impl From<Segment> for RenderItem {
173 fn from(s: Segment) -> Self {
174 Self::Segment(s)
175 }
176}
177
178impl From<DynRenderable> for RenderItem {
179 fn from(r: DynRenderable) -> Self {
180 Self::Nested(r)
181 }
182}
183
184#[derive(Debug, Clone)]
187pub struct RenderResult {
188 pub lines: Vec<Vec<Segment>>,
190 pub items: Vec<RenderItem>,
193}
194
195impl Default for RenderResult {
196 fn default() -> Self {
197 Self::new()
198 }
199}
200
201impl RenderResult {
202 pub fn new() -> Self {
204 Self {
205 lines: Vec::new(),
206 items: Vec::new(),
207 }
208 }
209
210 pub fn from_text(text: &str) -> Self {
214 Self {
215 lines: vec![vec![Segment::new(text)]],
216 items: vec![RenderItem::Segment(Segment::new(text))],
217 }
218 }
219
220 pub fn from_segments(segments: Vec<Segment>) -> Self {
222 let items: Vec<RenderItem> = segments
223 .iter()
224 .map(|s| RenderItem::Segment(s.clone()))
225 .collect();
226 Self {
227 lines: vec![segments],
228 items,
229 }
230 }
231
232 pub fn from_lines(lines: Vec<Vec<Segment>>) -> Self {
234 Self {
235 lines,
236 items: Vec::new(),
237 }
238 }
239
240 pub fn from_items(items: Vec<RenderItem>) -> Self {
242 Self {
243 lines: Vec::new(),
244 items,
245 }
246 }
247
248 pub fn push_item(&mut self, item: impl Into<RenderItem>) {
250 self.items.push(item.into());
251 }
252
253 pub fn push_renderable(&mut self, r: impl Renderable + Send + Sync + 'static) {
255 self.items.push(RenderItem::Nested(DynRenderable::new(r)));
256 }
257
258 pub fn flatten(&self, options: &ConsoleOptions) -> Vec<Segment> {
261 let mut out: Vec<Segment> = Vec::new();
262 flatten_items(&self.items, options, &mut out);
263 if out.is_empty() {
265 for line in &self.lines {
266 for seg in line {
267 out.push(seg.clone());
268 }
269 }
270 }
271 out
272 }
273
274 pub fn to_ansi(&self) -> String {
276 let mut out = String::new();
277 if !self.items.is_empty() {
279 let flat = self.flatten(&ConsoleOptions::default());
280 for seg in &flat {
281 out.push_str(&seg.to_ansi());
282 }
283 } else {
284 for line in &self.lines {
285 for seg in line {
286 out.push_str(&seg.to_ansi());
287 }
288 }
289 }
290 out
291 }
292}
293
294fn flatten_items(items: &[RenderItem], options: &ConsoleOptions, out: &mut Vec<Segment>) {
296 for item in items {
297 match item {
298 RenderItem::Segment(seg) => out.push(seg.clone()),
299 RenderItem::Nested(renderable) => {
300 let nested = renderable.render(options);
301 flatten_items(&nested.items, options, out);
302 }
303 }
304 }
305}
306
307pub trait Renderable {
311 fn render(&self, options: &ConsoleOptions) -> RenderResult;
316
317 fn measure(&self, _options: &ConsoleOptions) -> Option<crate::measure::Measurement> {
320 None
321 }
322}
323
324impl Renderable for String {
328 fn render(&self, options: &ConsoleOptions) -> RenderResult {
329 self.as_str().render(options)
330 }
331}
332
333impl Renderable for &str {
335 fn render(&self, _options: &ConsoleOptions) -> RenderResult {
336 RenderResult::from_text(self)
337 }
338}
339
340impl Renderable for Text {
342 fn render(&self, _options: &ConsoleOptions) -> RenderResult {
343 let rendered = self.render();
344 let lines: Vec<Vec<Segment>> = rendered.lines().map(|l| vec![Segment::new(l)]).collect();
346 RenderResult {
347 lines,
348 items: Vec::new(),
349 }
350 }
351}
352
353#[derive(Clone)]
358pub struct DynRenderable {
359 inner: Arc<dyn Renderable + Send + Sync>,
360}
361
362impl DynRenderable {
363 pub fn new(r: impl Renderable + Send + Sync + 'static) -> Self {
365 Self { inner: Arc::new(r) }
366 }
367}
368
369impl fmt::Debug for DynRenderable {
370 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
371 f.debug_struct("DynRenderable").finish()
372 }
373}
374
375impl Renderable for DynRenderable {
377 fn render(&self, options: &ConsoleOptions) -> RenderResult {
378 self.inner.render(options)
379 }
380
381 fn measure(&self, options: &ConsoleOptions) -> Option<crate::measure::Measurement> {
382 self.inner.measure(options)
383 }
384}
385
386#[derive(Debug, Clone)]
390pub struct Group {
391 pub children: Vec<DynRenderable>,
393}
394
395impl Default for Group {
396 fn default() -> Self {
397 Self::new()
398 }
399}
400
401impl Group {
402 pub fn new() -> Self {
404 Self {
405 children: Vec::new(),
406 }
407 }
408
409 pub fn add(&mut self, renderable: impl Renderable + Send + Sync + 'static) {
411 self.children.push(DynRenderable::new(renderable));
412 }
413}
414
415impl Renderable for Group {
417 fn render(&self, options: &ConsoleOptions) -> RenderResult {
418 let mut all_lines: Vec<Vec<Segment>> = Vec::new();
419 for child in &self.children {
420 let result = child.render(options);
421 all_lines.extend(result.lines);
422 }
423 RenderResult {
424 lines: all_lines,
425 items: Vec::new(),
426 }
427 }
428}
429
430struct CaptureWriter {
436 buf: Arc<Mutex<Vec<u8>>>,
437}
438
439impl Write for CaptureWriter {
440 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
441 let mut data = self.buf.lock().unwrap();
442 data.extend_from_slice(buf);
443 Ok(buf.len())
444 }
445 fn flush(&mut self) -> io::Result<()> {
446 Ok(())
447 }
448}
449
450#[derive(Debug)]
452pub struct Capture {
453 buf: Arc<Mutex<Vec<u8>>>,
454}
455
456impl Capture {
457 pub fn new(_console: &Console) -> Self {
459 Self {
460 buf: Arc::new(Mutex::new(Vec::new())),
461 }
462 }
463
464 pub fn get(&self) -> String {
466 let data = self.buf.lock().unwrap();
467 String::from_utf8_lossy(&data).to_string()
468 }
469}
470
471pub use crate::pager::{Pager, PagerContext, SystemPager};
473
474#[derive(Debug, Clone, PartialEq, Eq)]
480pub enum CaptureError {
481 AlreadyCapturing,
483 NotCapturing,
485 InvalidUtf8,
487}
488
489impl fmt::Display for CaptureError {
490 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
491 match self {
492 Self::AlreadyCapturing => write!(f, "capture already in progress"),
493 Self::NotCapturing => write!(f, "no capture active"),
494 Self::InvalidUtf8 => write!(f, "captured output is not valid UTF-8"),
495 }
496 }
497}
498
499impl std::error::Error for CaptureError {}
500
501pub struct NewLine;
507
508impl Renderable for NewLine {
509 fn render(&self, _options: &ConsoleOptions) -> RenderResult {
510 RenderResult::from_text("\n")
511 }
512}
513
514pub struct NoChange;
516
517impl Renderable for NoChange {
518 fn render(&self, _options: &ConsoleOptions) -> RenderResult {
519 RenderResult::new()
520 }
521}
522
523pub type RenderHookFn = dyn Fn(&[Vec<Segment>]) -> Vec<Vec<Segment>> + Send;
529
530pub struct RenderHook {
532 hook: Box<RenderHookFn>,
533}
534
535impl RenderHook {
536 pub fn new<F: Fn(&[Vec<Segment>]) -> Vec<Vec<Segment>> + Send + 'static>(f: F) -> Self {
538 Self { hook: Box::new(f) }
539 }
540
541 pub fn apply(&self, lines: &[Vec<Segment>]) -> Vec<Vec<Segment>> {
543 (self.hook)(lines)
544 }
545}
546
547impl fmt::Debug for RenderHook {
548 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
549 f.debug_struct("RenderHook").finish()
550 }
551}
552
553pub struct ThemeContext<'a> {
567 _phantom: std::marker::PhantomData<&'a mut Console>,
568 console_ptr: *mut Console,
569 previous_theme: Theme,
570 _not_send_sync: std::marker::PhantomData<*const ()>,
572}
573
574impl<'a> ThemeContext<'a> {
579 pub(crate) fn new(console: &'a mut Console, previous_theme: Theme) -> Self {
581 Self {
582 _phantom: std::marker::PhantomData,
583 console_ptr: console as *mut Console,
584 previous_theme,
585 _not_send_sync: std::marker::PhantomData,
586 }
587 }
588}
589
590impl<'a> Drop for ThemeContext<'a> {
591 fn drop(&mut self) {
592 unsafe {
593 (*self.console_ptr).theme = std::mem::take(&mut self.previous_theme);
594 }
595 }
596}
597
598pub struct Console {
604 pub file: Box<dyn Write + Send>,
606 pub color_system: ColorSystem,
608 pub theme: Theme,
610 pub options: ConsoleOptions,
612 width: Option<usize>,
614 height: Option<usize>,
616 is_terminal: bool,
618 pub quiet: bool,
620 pub soft_wrap: bool,
622 alt_screen: bool,
624 cursor_visible: bool,
626 render_hooks: Vec<RenderHook>,
628 capture_buf: Option<Arc<Mutex<Vec<u8>>>>,
630 saved_file: Option<Box<dyn Write + Send>>,
632}
633
634impl Console {
635 pub fn new() -> Self {
637 let is_terminal = std::io::stdout().is_terminal();
638 let color_system = detect_color_system();
639
640 let size = ConsoleDimensions::detect();
641 let render_width = size.width.saturating_sub(1);
645
646 Self {
647 file: Box::new(io::stdout()) as Box<dyn Write + Send>,
648 color_system,
649 theme: crate::theme::default_theme(),
650 options: ConsoleOptions {
651 size,
652 is_terminal,
653 max_width: render_width,
654 max_height: size.height,
655 ..Default::default()
656 },
657 width: None,
658 height: None,
659 is_terminal,
660 quiet: false,
661 soft_wrap: false,
662 alt_screen: false,
663 cursor_visible: true,
664 render_hooks: Vec::new(),
665 capture_buf: None,
666 saved_file: None,
667 }
668 }
669
670 pub fn with_file(file: Box<dyn Write + Send>) -> Self {
672 let _is_terminal = false;
673 Self {
674 file,
675 color_system: ColorSystem::Standard,
676 theme: crate::theme::default_theme(),
677 options: ConsoleOptions {
678 size: ConsoleDimensions {
679 width: 80,
680 height: 25,
681 },
682 is_terminal: false,
683 max_width: 80,
684 max_height: 25,
685 ..Default::default()
686 },
687 width: None,
688 height: None,
689 is_terminal: false,
690 quiet: false,
691 soft_wrap: false,
692 alt_screen: false,
693 cursor_visible: true,
694 render_hooks: Vec::new(),
695 capture_buf: None,
696 saved_file: None,
697 }
698 }
699
700 pub fn set_width(&mut self, width: usize) {
702 self.width = Some(width);
703 self.options.max_width = width;
704 }
705
706 pub fn set_height(&mut self, height: usize) {
708 self.height = Some(height);
709 self.options.max_height = height;
710 }
711
712 pub fn width(&self) -> usize {
714 self.width.unwrap_or(self.options.size.width)
715 }
716
717 pub fn height(&self) -> usize {
719 self.height.unwrap_or(self.options.size.height)
720 }
721
722 pub fn render_lines(
724 &self,
725 renderable: &dyn Renderable,
726 options: &ConsoleOptions,
727 style: Option<&Style>,
728 _pad: bool,
729 ) -> Vec<Vec<Segment>> {
730 let result = renderable.render(options);
731
732 if let Some(st) = style {
733 result
734 .lines
735 .into_iter()
736 .map(|line| {
737 line.into_iter()
738 .map(|seg| {
739 let new_style = if let Some(ref s) = seg.style {
740 s.combine(st)
741 } else {
742 st.clone()
743 };
744 Segment::styled(seg.text, new_style)
745 })
746 .collect()
747 })
748 .collect()
749 } else {
750 result.lines
751 }
752 }
753
754 pub fn get_style(&self, name: &str, default: &str) -> Option<Style> {
756 self.theme.get(name).cloned().or_else(|| {
757 if !default.is_empty() {
758 Some(Style::from_str(default))
759 } else {
760 None
761 }
762 })
763 }
764
765 pub fn render_str(&self, text: &str, style: &str) -> Text {
767 let st = self.get_style(style, "");
768 let mut t = Text::new(text);
769 if let Some(s) = st {
770 t = t.style(s);
771 }
772 t
773 }
774
775 pub fn print(&mut self, objects: &[&dyn Renderable], sep: &str, end: &str) {
782 if self.quiet {
783 return;
784 }
785 let mut first = true;
786 for obj in objects {
787 if !first {
788 let _ = write!(self.file, "{sep}");
789 }
790 first = false;
791 let result = obj.render(&self.options);
792 let ansi = result.to_ansi();
793 let _ = write!(self.file, "{ansi}");
794 }
795 let _ = write!(self.file, "{end}");
796 let _ = self.file.flush();
797 }
798
799 pub fn println(&mut self, renderable: &dyn Renderable) {
804 if self.quiet {
805 return;
806 }
807 self.refresh_size();
808 let result = renderable.render(&self.options);
809 let ansi = result.to_ansi();
810 let _ = writeln!(self.file, "{ansi}");
811 let _ = self.file.flush();
812 }
813
814 fn refresh_size(&mut self) {
816 if self.is_terminal {
817 let size = ConsoleDimensions::detect();
818 self.options.size = size;
819 self.options.max_width = size.width.saturating_sub(1);
821 self.options.max_height = size.height;
822 }
823 }
824
825 pub fn print_str(&mut self, text: &str) {
828 if self.quiet {
829 return;
830 }
831 let ansi = if self.options.markup {
832 let parsed = crate::markup::render(text);
833 parsed.render()
834 } else {
835 crate::export::strip_ansi_escapes(text)
837 };
838 let _ = write!(self.file, "{ansi}");
839 let _ = self.file.flush();
840 }
841
842 pub fn print_json(&mut self, data: &serde_json::Value) {
844 if self.quiet {
845 return;
846 }
847 let formatted = crate::json::render_json(data);
848 let result = formatted.render(&self.options);
849 let ansi = result.to_ansi();
850 let _ = writeln!(self.file, "{ansi}");
851 let _ = self.file.flush();
852 }
853
854 pub fn clear(&mut self) {
856 if self.quiet {
857 return;
858 }
859 let _ = write!(self.file, "{}", crate::control::CLEAR_HOME);
860 let _ = self.file.flush();
861 }
862
863 pub fn show_cursor(&mut self) {
865 self.cursor_visible = true;
866 let _ = write!(self.file, "{}", crate::control::CURSOR_SHOW);
867 let _ = self.file.flush();
868 }
869
870 pub fn hide_cursor(&mut self) {
872 self.cursor_visible = false;
873 let _ = write!(self.file, "{}", crate::control::CURSOR_HIDE);
874 let _ = self.file.flush();
875 }
876
877 pub fn set_window_title(&mut self, title: &str) {
879 let _ = write!(
880 self.file,
881 "{}{}{}",
882 crate::control::OSC,
883 title,
884 crate::control::ST
885 );
886 let _ = self.file.flush();
887 }
888
889 pub fn color_ansi(&self, color: &Color) -> String {
891 let downgraded = color.downgrade(self.color_system);
892 downgraded.to_string()
893 }
894
895 pub fn render(&self, renderable: &dyn Renderable, options: &ConsoleOptions) -> Vec<Segment> {
902 let result = renderable.render(options);
903 result.flatten(options)
904 }
905
906 pub fn measure(
909 &self,
910 renderable: &dyn Renderable,
911 options: &ConsoleOptions,
912 ) -> crate::measure::Measurement {
913 if let Some(m) = renderable.measure(options) {
914 return m;
915 }
916 let segments = self.render(renderable, options);
917 let max_w = segments.iter().map(|s| s.cell_length()).max().unwrap_or(0);
918 crate::measure::Measurement::new(max_w, options.max_width)
919 }
920
921 pub fn rule(
926 &mut self,
927 title: impl Into<String>,
928 characters: Option<&str>,
929 style: Option<Style>,
930 align: Option<AlignMethod>,
931 ) {
932 if self.quiet {
933 return;
934 }
935 let mut rule = crate::rule::Rule::new().title(title);
936 if let Some(chars) = characters {
937 rule = rule.characters(chars);
938 }
939 if let Some(st) = style {
940 rule = rule.style(st);
941 }
942 if let Some(a) = align {
943 rule = rule.align(a);
944 }
945 let result = rule.render(&self.options);
946 let ansi = result.to_ansi();
947 let _ = write!(self.file, "{ansi}");
948 let _ = self.file.flush();
949 }
950
951 pub fn bell(&mut self) {
953 if self.quiet {
954 return;
955 }
956 let _ = write!(self.file, "\x07");
957 let _ = self.file.flush();
958 }
959
960 pub fn line(&mut self, count: usize) {
962 if self.quiet {
963 return;
964 }
965 for _ in 0..count {
966 let _ = writeln!(self.file);
967 }
968 let _ = self.file.flush();
969 }
970
971 pub fn log(&mut self, objects: &[&dyn Renderable]) {
973 if self.quiet {
974 return;
975 }
976 let now = chrono::Local::now();
977 let time_str = format!("[{}]", now.format("%H:%M:%S"));
978 let _ = write!(self.file, "{} ", Style::new().dim(true).to_ansi());
979 let _ = write!(self.file, "{time_str} ");
980 let _ = write!(self.file, "{}", Style::new().reset_ansi());
981 self.print(objects, " ", "\n");
982 }
983
984 pub fn push_theme(&mut self, theme: Theme) {
988 let mut new_theme = theme.clone();
989 new_theme.inherit = Some(Box::new(self.theme.clone()));
990 self.theme = new_theme;
991 }
992
993 pub fn pop_theme(&mut self) {
995 if let Some(ref inherit) = self.theme.inherit {
996 self.theme = *inherit.clone();
997 }
998 }
999
1000 pub fn export_html(&self, renderable: &dyn Renderable) -> String {
1010 let segments = self.render(renderable, &self.options);
1011 let code =
1012 crate::export::segments_to_html(&segments, &crate::export::ExportTheme::default());
1013 crate::export::export_html(&crate::export::ExportHtmlOptions {
1014 code,
1015 ..Default::default()
1016 })
1017 }
1018
1019 pub fn save_html(
1021 &self,
1022 path: impl AsRef<std::path::Path>,
1023 renderable: &dyn Renderable,
1024 ) -> std::io::Result<()> {
1025 let html = self.export_html(renderable);
1026 std::fs::write(path.as_ref(), html)
1027 }
1028
1029 pub fn export_svg(&self, renderable: &dyn Renderable) -> String {
1035 let segments = self.render(renderable, &self.options);
1036 let code =
1037 crate::export::segments_to_svg(&segments, &crate::export::ExportTheme::default());
1038 crate::export::export_svg(&crate::export::ExportSvgOptions {
1039 code,
1040 ..Default::default()
1041 })
1042 }
1043
1044 pub fn save_svg(
1046 &self,
1047 path: impl AsRef<std::path::Path>,
1048 renderable: &dyn Renderable,
1049 ) -> std::io::Result<()> {
1050 let svg = self.export_svg(renderable);
1051 crate::export::save_svg(
1052 path,
1053 &crate::export::ExportSvgOptions {
1054 code: svg,
1055 ..Default::default()
1056 },
1057 )
1058 }
1059
1060 pub fn export_text(&self, renderable: &dyn Renderable) -> String {
1062 let result = renderable.render(&self.options);
1063 let ansi = result.to_ansi();
1064 crate::export::export_text(&crate::export::ExportTextOptions {
1065 text: ansi,
1066 strip_ansi: true,
1067 })
1068 }
1069
1070 pub fn save_text(
1072 &self,
1073 path: impl AsRef<std::path::Path>,
1074 renderable: &dyn Renderable,
1075 ) -> std::io::Result<()> {
1076 let text = self.export_text(renderable);
1077 crate::export::save_text(
1078 path,
1079 &crate::export::ExportTextOptions {
1080 text,
1081 strip_ansi: false,
1082 },
1083 )
1084 }
1085
1086 pub fn set_quiet(&mut self, quiet: bool) {
1090 self.quiet = quiet;
1091 }
1092
1093 pub fn quiet(mut self, quiet: bool) -> Self {
1095 self.quiet = quiet;
1096 self
1097 }
1098
1099 pub fn set_soft_wrap(&mut self, soft_wrap: bool) {
1101 self.soft_wrap = soft_wrap;
1102 }
1103
1104 pub fn soft_wrap(mut self, soft_wrap: bool) -> Self {
1106 self.soft_wrap = soft_wrap;
1107 self
1108 }
1109
1110 pub fn input(&mut self, prompt: &str, password: bool) -> String {
1118 let _ = write!(self.file, "{prompt}");
1119 let _ = self.file.flush();
1120
1121 if password {
1122 self.read_password()
1123 } else {
1124 let mut input = String::new();
1125 let _ = io::stdin().read_line(&mut input);
1126 input.trim().to_string()
1127 }
1128 }
1129
1130 fn read_password(&mut self) -> String {
1132 use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
1133 use std::io::Read;
1134
1135 match enable_raw_mode() {
1136 Ok(()) => {
1137 let stdin = io::stdin();
1138 let mut handle = stdin.lock();
1139 let mut buf = [0u8; 1];
1140 let mut password = String::new();
1141
1142 while let Ok(()) = handle.read_exact(&mut buf) {
1143 match buf[0] {
1144 b'\r' | b'\n' => {
1145 let _ = writeln!(self.file);
1146 let _ = self.file.flush();
1147 break;
1148 }
1149 b'\x03' => {
1150 let _ = writeln!(self.file);
1152 let _ = self.file.flush();
1153 break;
1154 }
1155 b'\x7f' | b'\x08' => {
1156 password.pop();
1158 }
1159 c => {
1160 password.push(c as char);
1161 let _ = write!(self.file, "*");
1162 let _ = self.file.flush();
1163 }
1164 }
1165 }
1166 let _ = disable_raw_mode();
1167 password
1168 }
1169 Err(_) => {
1170 let mut input = String::new();
1172 let _ = io::stdin().read_line(&mut input);
1173 input.trim().to_string()
1174 }
1175 }
1176 }
1177
1178 pub fn screen(&mut self) -> crate::screen::ScreenContext {
1184 let mut ctx = crate::screen::ScreenContext::new();
1185 ctx.enter();
1186 ctx
1187 }
1188
1189 pub fn set_alt_screen(&mut self, enable: bool) {
1192 self.alt_screen = enable;
1193 let seq = if enable {
1194 crate::control::ALT_SCREEN_ENTER
1195 } else {
1196 crate::control::ALT_SCREEN_EXIT
1197 };
1198 let _ = write!(self.file, "{seq}");
1199 let _ = self.file.flush();
1200 }
1201
1202 pub fn is_terminal(&self) -> bool {
1204 self.is_terminal
1205 }
1206
1207 pub fn set_size(&mut self, width: usize, height: usize) {
1209 self.width = Some(width);
1210 self.height = Some(height);
1211 self.options.max_width = width;
1212 self.options.max_height = height;
1213 self.options.size = crate::console::ConsoleDimensions { width, height };
1214 }
1215
1216 pub fn on_broken_pipe(&self) {
1224 }
1228}
1229
1230impl Default for Console {
1231 fn default() -> Self {
1232 Self::new()
1233 }
1234}
1235
1236impl fmt::Debug for Console {
1237 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1238 f.debug_struct("Console")
1239 .field("color_system", &self.color_system)
1240 .field("width", &self.width())
1241 .field("height", &self.height())
1242 .field("is_terminal", &self.is_terminal)
1243 .field("alt_screen", &self.alt_screen)
1244 .field("cursor_visible", &self.cursor_visible)
1245 .field("quiet", &self.quiet)
1246 .field("soft_wrap", &self.soft_wrap)
1247 .finish()
1248 }
1249}
1250
1251impl Console {
1256 pub fn begin_capture(&mut self) {
1262 let buf = Arc::new(Mutex::new(Vec::new()));
1263 let writer = Box::new(CaptureWriter { buf: buf.clone() });
1264 self.saved_file = Some(std::mem::replace(&mut self.file, writer));
1265 self.capture_buf = Some(buf);
1266 }
1267
1268 pub fn end_capture(&mut self) -> Result<Capture, CaptureError> {
1272 let buf = self.capture_buf.take().ok_or(CaptureError::NotCapturing)?;
1273 if let Some(saved) = self.saved_file.take() {
1274 self.file = saved;
1275 }
1276 Ok(Capture { buf })
1277 }
1278
1279 pub fn capture<F: FnOnce(&mut Self)>(&mut self, f: F) -> Result<String, CaptureError> {
1292 self.begin_capture();
1293 f(self);
1294 self.end_capture().map(|cap| cap.get())
1295 }
1296
1297 pub fn pager(&mut self, styles: bool) -> PagerContext {
1305 PagerContext::new(Pager::new().color(styles))
1306 }
1307
1308 pub fn input_renderable(&mut self, prompt: &dyn Renderable) -> String {
1314 if !self.quiet {
1315 let result = prompt.render(&self.options);
1316 let ansi = result.to_ansi();
1317 let _ = write!(self.file, "{ansi}");
1318 let _ = self.file.flush();
1319 }
1320 let mut input = String::new();
1321 let _ = io::stdin().read_line(&mut input);
1322 input.trim().to_string()
1323 }
1324
1325 pub fn print_exception(&mut self, _width: Option<usize>, _extra_lines: usize) {
1334 if self.quiet {
1335 return;
1336 }
1337 let msg = "[bold red]Exception[/bold red]: No current exception info. ";
1342 let msg_text = crate::text::Text::from_markup(msg);
1343 let result = msg_text.render();
1344 let _ = writeln!(self.file, "{result}");
1345 let _ = self.file.flush();
1346 }
1347
1348 pub fn print_json_str(&mut self, json: &str) {
1353 if self.quiet {
1354 return;
1355 }
1356 if let Ok(value) = serde_json::from_str::<serde_json::Value>(json) {
1357 self.print_json(&value);
1358 } else {
1359 let _ = writeln!(self.file, "[invalid JSON]");
1360 let _ = self.file.flush();
1361 }
1362 }
1363
1364 pub fn render_to_lines(
1372 &self,
1373 renderable: &dyn Renderable,
1374 options: &ConsoleOptions,
1375 ) -> Vec<Vec<Segment>> {
1376 let result = renderable.render(options);
1377 let has_items = !result.items.is_empty();
1378 let mut lines = if result.lines.is_empty() && has_items {
1379 let flat = result.flatten(options);
1380 if flat.is_empty() {
1381 Vec::new() } else {
1383 vec![flat]
1384 }
1385 } else {
1386 result.lines
1387 };
1388 if !self.render_hooks.is_empty() {
1390 for hook in &self.render_hooks {
1391 lines = hook.apply(&lines);
1392 }
1393 }
1394 lines
1395 }
1396
1397 pub fn render_ansi(&self, text: &str) -> String {
1402 let t = self.render_str(text, "");
1403 t.render()
1404 }
1405
1406 pub fn export_svg_opts(&self, options: &crate::export::ExportSvgOptions) -> String {
1413 crate::export::export_svg(options)
1414 }
1415
1416 pub fn size(&self) -> ConsoleDimensions {
1420 ConsoleDimensions {
1421 width: self.width(),
1422 height: self.height(),
1423 }
1424 }
1425
1426 pub fn is_dumb_terminal(&self) -> bool {
1428 std::env::var("TERM").is_ok_and(|t| t == "dumb")
1429 }
1430
1431 pub fn is_alt_screen(&self) -> bool {
1433 self.alt_screen
1434 }
1435
1436 pub fn set_cursor_visible(&mut self, visible: bool) {
1443 self.cursor_visible = visible;
1444 let seq = if visible {
1445 crate::control::CURSOR_SHOW
1446 } else {
1447 crate::control::CURSOR_HIDE
1448 };
1449 let _ = write!(self.file, "{seq}");
1450 let _ = self.file.flush();
1451 }
1452
1453 pub fn use_theme(&mut self, theme: Theme) -> ThemeContext<'_> {
1469 let prev = std::mem::replace(&mut self.theme, theme);
1470 ThemeContext::new(self, prev)
1471 }
1472
1473 pub fn clear_live(&mut self) {
1477 let _ = write!(self.file, "{}", crate::control::CLEAR_HOME);
1478 let _ = self.file.flush();
1479 }
1480
1481 pub fn set_live(&mut self, _live: &crate::live::Live) {
1488 }
1491
1492 pub fn update_screen(&mut self, renderable: &dyn Renderable, options: Option<&ConsoleOptions>) {
1497 let opts = options.unwrap_or(&self.options);
1498 let segments = self.render(renderable, opts);
1499 let mut output = String::new();
1500 for seg in &segments {
1501 output.push_str(&seg.to_ansi());
1502 }
1503 let _ = write!(self.file, "{}{output}", crate::control::CLEAR_HOME);
1504 let _ = self.file.flush();
1505 }
1506
1507 pub fn update_screen_lines(
1512 &mut self,
1513 lines: &[Vec<Segment>],
1514 options: Option<&ConsoleOptions>,
1515 ) {
1516 let _ = options;
1517 let mut output = String::new();
1518 for line in lines {
1519 for seg in line {
1520 output.push_str(&seg.to_ansi());
1521 }
1522 output.push('\n');
1523 }
1524 let _ = write!(self.file, "{}{output}", crate::control::CLEAR_HOME);
1525 let _ = self.file.flush();
1526 }
1527
1528 pub fn push_render_hook(&mut self, hook: RenderHook) {
1533 self.render_hooks.push(hook);
1534 }
1535
1536 pub fn pop_render_hook(&mut self) -> Option<RenderHook> {
1538 self.render_hooks.pop()
1539 }
1540}
1541
1542fn detect_color_system() -> ColorSystem {
1551 if let Ok(val) = std::env::var("COLORTERM") {
1553 if val == "truecolor" || val == "24bit" {
1554 return ColorSystem::TrueColor;
1555 }
1556 }
1557 if let Ok(term) = std::env::var("TERM") {
1558 if term.contains("256color") {
1559 return ColorSystem::EightBit;
1560 }
1561 if term == "xterm-kitty" {
1562 return ColorSystem::TrueColor;
1563 }
1564 }
1565 if std::env::var("NO_COLOR").is_ok() {
1567 return ColorSystem::Standard;
1568 }
1569 if std::io::stdout().is_terminal() {
1571 ColorSystem::TrueColor
1572 } else {
1573 ColorSystem::Standard
1574 }
1575}
1576
1577use std::sync::LazyLock;
1582
1583static GLOBAL_CONSOLE: LazyLock<Mutex<Console>> = LazyLock::new(|| Mutex::new(Console::new()));
1584
1585pub fn get_console() -> std::sync::MutexGuard<'static, Console> {
1587 GLOBAL_CONSOLE.lock().unwrap_or_else(|e| e.into_inner())
1588}
1589
1590pub fn print_objects(objects: &[&dyn Renderable]) {
1596 let mut console = GLOBAL_CONSOLE.lock().unwrap();
1597 console.print(objects, " ", "\n");
1598}
1599
1600pub fn print_str(text: &str) {
1602 let mut console = GLOBAL_CONSOLE.lock().unwrap();
1603 console.print_str(text);
1604}
1605
1606pub fn print_json_val(data: &serde_json::Value) {
1608 let mut console = GLOBAL_CONSOLE.lock().unwrap();
1609 console.print_json(data);
1610}
1611
1612pub fn reconfigure(width: Option<usize>, height: Option<usize>, color_system: Option<ColorSystem>) {
1626 let mut console = GLOBAL_CONSOLE.lock().unwrap();
1627 if let Some(w) = width {
1628 console.set_width(w);
1629 }
1630 if let Some(h) = height {
1631 console.set_height(h);
1632 }
1633 if let Some(cs) = color_system {
1634 console.color_system = cs;
1635 }
1636}
1637
1638#[cfg(test)]
1639mod tests {
1640 use super::*;
1641
1642 #[test]
1643 fn test_render_result_from_text() {
1644 let r = RenderResult::from_text("hello");
1645 assert_eq!(r.lines.len(), 1);
1646 assert_eq!(r.lines[0][0].text, "hello");
1647 }
1648
1649 #[test]
1650 fn test_console_options_default() {
1651 let opts = ConsoleOptions::default();
1652 assert!(opts.markup);
1653 }
1654
1655 #[test]
1656 fn test_console_quiet_default() {
1657 let console = Console::new();
1658 assert!(!console.quiet);
1659 }
1660
1661 #[test]
1662 fn test_console_quiet_setter() {
1663 let mut console = Console::new();
1664 console.set_quiet(true);
1665 assert!(console.quiet);
1666 }
1667
1668 #[test]
1669 fn test_console_quiet_builder() {
1670 let console = Console::new().quiet(true);
1671 assert!(console.quiet);
1672 }
1673
1674 #[test]
1675 fn test_console_quiet_suppresses_print() {
1676 let mut console = Console::new();
1677 console.quiet = true;
1678 console.print(&[], " ", "\n");
1680 console.println(&"test");
1681 console.print_str("test");
1682 }
1683
1684 #[test]
1685 fn test_console_soft_wrap_default() {
1686 let console = Console::new();
1687 assert!(!console.soft_wrap);
1688 }
1689
1690 #[test]
1691 fn test_console_soft_wrap_setter() {
1692 let mut console = Console::new();
1693 console.set_soft_wrap(true);
1694 assert!(console.soft_wrap);
1695 }
1696
1697 #[test]
1698 fn test_console_soft_wrap_builder() {
1699 let console = Console::new().soft_wrap(true);
1700 assert!(console.soft_wrap);
1701 }
1702
1703 #[test]
1704 fn test_console_is_terminal() {
1705 let console = Console::new();
1706 let detected = console.is_terminal();
1708 assert_eq!(detected, std::io::stdout().is_terminal());
1709 }
1710
1711 #[test]
1712 fn test_console_set_size() {
1713 let mut console = Console::new();
1714 console.set_size(120, 30);
1715 assert_eq!(console.width(), 120);
1716 assert_eq!(console.height(), 30);
1717 assert_eq!(console.options.max_width, 120);
1718 assert_eq!(console.options.max_height, 30);
1719 }
1720
1721 #[test]
1722 fn test_console_set_alt_screen() {
1723 let mut console = Console::new();
1724 console.set_alt_screen(true);
1726 console.set_alt_screen(false);
1727 }
1728
1729 #[test]
1730 fn test_console_on_broken_pipe() {
1731 let console = Console::new();
1732 console.on_broken_pipe(); }
1734
1735 #[test]
1736 fn test_console_input_normal() {
1737 let _console = Console::new();
1740 }
1742
1743 #[test]
1744 fn test_console_debug() {
1745 let console = Console::new();
1746 let debug = format!("{:?}", console);
1747 assert!(debug.contains("Console"));
1748 }
1749
1750 #[test]
1751 fn test_console_with_file_has_no_terminal() {
1752 let console = Console::with_file(Box::new(std::io::sink()));
1753 assert!(!console.is_terminal());
1754 }
1755
1756 #[test]
1759 fn test_newline_renderable() {
1760 let nl = NewLine;
1761 let result = nl.render(&ConsoleOptions::default());
1762 let ansi = result.to_ansi();
1763 assert_eq!(ansi, "\n");
1764 }
1765
1766 #[test]
1767 fn test_nochange_renderable() {
1768 let nc = NoChange;
1769 let result = nc.render(&ConsoleOptions::default());
1770 assert!(result.lines.is_empty());
1771 assert!(result.items.is_empty());
1772 }
1773
1774 #[test]
1775 fn test_capture_begin_end() {
1776 let mut console = Console::with_file(Box::new(std::io::sink()));
1777 console.begin_capture();
1778 let _ = write!(console.file, "captured text");
1779 let cap = console.end_capture().unwrap();
1780 assert_eq!(cap.get(), "captured text");
1781 }
1782
1783 #[test]
1784 fn test_capture_with_closure() {
1785 let mut console = Console::with_file(Box::new(std::io::sink()));
1786 let output = console
1787 .capture(|c| {
1788 let _ = write!(c.file, "hello from capture");
1789 })
1790 .unwrap();
1791 assert_eq!(output, "hello from capture");
1792 }
1793
1794 #[test]
1795 fn test_capture_new_empty() {
1796 let console = Console::new();
1797 let cap = Capture::new(&console);
1798 assert_eq!(cap.get(), "");
1799 }
1800
1801 #[test]
1802 fn test_end_capture_not_capturing() {
1803 let mut console = Console::new();
1804 let result = console.end_capture();
1805 assert!(result.is_err());
1806 assert_eq!(result.unwrap_err(), CaptureError::NotCapturing);
1807 }
1808
1809 #[test]
1810 fn test_system_pager_default() {
1811 let pager = SystemPager::new();
1812 let _ = pager.show("");
1815 }
1816
1817 #[test]
1818 fn test_pager_enabled() {
1819 let pager = Pager::new();
1820 assert!(pager.is_enabled());
1821 let disabled = pager.enabled(false);
1822 assert!(!disabled.is_enabled());
1823 }
1824
1825 #[test]
1826 fn test_render_hook() {
1827 let hook = RenderHook::new(|lines| {
1828 let hooked: Vec<Vec<Segment>> = lines
1830 .iter()
1831 .map(|line| {
1832 let mut new_line = line.clone();
1833 new_line.push(Segment::styled("HOOKED", Style::new().bold(true)));
1834 new_line
1835 })
1836 .collect();
1837 hooked
1838 });
1839 let lines = vec![vec![Segment::new("test")]];
1840 let result = hook.apply(&lines);
1841 assert_eq!(result.len(), 1);
1842 assert_eq!(result[0].len(), 2);
1843 assert_eq!(result[0][1].text, "HOOKED");
1844 }
1845
1846 #[test]
1847 fn test_console_size() {
1848 let mut console = Console::new();
1849 console.set_size(100, 40);
1850 let dims = console.size();
1851 assert_eq!(dims.width, 100);
1852 assert_eq!(dims.height, 40);
1853 }
1854
1855 #[test]
1856 fn test_console_is_dumb_terminal() {
1857 let console = Console::new();
1858 let _ = console.is_dumb_terminal();
1861 }
1862
1863 #[test]
1864 fn test_console_is_alt_screen() {
1865 let mut console = Console::new();
1866 assert!(!console.is_alt_screen());
1867 console.alt_screen = true;
1868 assert!(console.is_alt_screen());
1869 }
1870
1871 #[test]
1872 fn test_console_render_ansi() {
1873 let console = Console::new();
1874 let ansi = console.render_ansi("test");
1875 assert!(ansi.contains("test") || ansi.contains("\x1b["));
1877 }
1878
1879 #[test]
1880 fn test_console_render_to_lines() {
1881 let console = Console::new();
1882 let opts = ConsoleOptions::default();
1883 let lines = console.render_to_lines(&"hello", &opts);
1884 assert_eq!(lines.len(), 1);
1885 assert_eq!(lines[0][0].text, "hello");
1886 }
1887
1888 #[test]
1889 fn test_console_input_renderable() {
1890 }
1893
1894 #[test]
1895 fn test_console_print_exception_noop() {
1896 let mut console = Console::new();
1897 console.print_exception(None, 3);
1899 }
1900
1901 #[test]
1902 fn test_console_render_hooks_push_pop() {
1903 let mut console = Console::new();
1904 let hook = RenderHook::new(|lines| lines.to_vec());
1905 console.push_render_hook(hook);
1906 assert_eq!(console.render_hooks.len(), 1);
1907 let popped = console.pop_render_hook();
1908 assert!(popped.is_some());
1909 assert!(console.render_hooks.is_empty());
1910 }
1911
1912 #[test]
1913 fn test_console_reconfigure() {
1914 reconfigure(Some(120), Some(40), None);
1916 reconfigure(None, None, Some(ColorSystem::Standard));
1917 reconfigure(None, None, None);
1919 }
1920
1921 #[test]
1922 fn test_pager_context_write() {
1923 let pager = Pager::new().enabled(false);
1924 let mut ctx = PagerContext::new(pager);
1925 ctx.feed("test content");
1926 }
1928
1929 #[test]
1930 fn test_theme_context() {
1931 let mut console = Console::new();
1932 let custom_theme = Theme::new();
1933 let original = console.theme.clone();
1934 {
1935 let _ctx = console.use_theme(custom_theme);
1936 }
1938 assert_eq!(console.theme.styles.len(), original.styles.len());
1940 }
1941}