1pub mod chrome;
42pub mod clipboard;
44mod display;
45pub mod document;
46pub(crate) mod document_model;
47pub(crate) mod format;
48pub mod inline;
50pub mod interactive;
51mod layout;
52pub mod messages;
53pub(crate) mod presentation;
54mod renderer;
55mod resolution;
56pub mod style;
58pub mod theme;
60pub(crate) mod theme_loader;
61mod width;
62
63use crate::core::output::{ColorMode, OutputFormat, RenderMode, UnicodeMode};
64use crate::core::output_model::{OutputItems, OutputResult};
65use crate::core::row::Row;
66use crate::guide::GuideView;
67
68pub use chrome::{
69 RuledSectionPolicy, SectionFrameStyle, SectionRenderContext, SectionStyleTokens,
70 render_section_block_with_overrides, render_section_divider_with_overrides,
71};
72pub use clipboard::{ClipboardError, ClipboardService};
73pub use document::{
74 Block, CodeBlock, Document, JsonBlock, LineBlock, LinePart, MregBlock, MregEntry, MregRow,
75 MregValue, PanelBlock, PanelRules, TableAlign, TableBlock, TableStyle, ValueBlock,
76};
77pub use inline::{line_from_inline, parts_from_inline, render_inline};
78pub use interactive::{Interactive, InteractiveResult, InteractiveRuntime, Spinner};
79pub use messages::{
80 GroupedRenderOptions, MessageBuffer, MessageLayout, MessageLevel, UiMessage, adjust_verbosity,
81};
82pub(crate) use resolution::ResolvedGuideRenderSettings;
83#[cfg(test)]
84pub(crate) use resolution::ResolvedHelpChromeSettings;
85pub(crate) use resolution::ResolvedRenderPlan;
86pub use resolution::ResolvedRenderSettings;
87pub use style::{StyleOverrides, StyleToken};
88pub use theme::{
89 DEFAULT_THEME_NAME, ThemeDefinition, ThemeOverrides, ThemePalette, all_themes,
90 available_theme_names, builtin_themes, display_name_from_id, find_builtin_theme, find_theme,
91 is_known_theme, normalize_theme_name, resolve_theme,
92};
93
94#[derive(Debug, Clone, Default, PartialEq, Eq)]
96#[non_exhaustive]
97pub struct RenderRuntime {
98 pub stdout_is_tty: bool,
100 pub terminal: Option<String>,
102 pub no_color: bool,
104 pub width: Option<usize>,
106 pub locale_utf8: Option<bool>,
108}
109
110impl RenderRuntime {
111 pub fn builder() -> RenderRuntimeBuilder {
113 RenderRuntimeBuilder::new()
114 }
115}
116
117#[derive(Debug, Clone, Default)]
119pub struct RenderRuntimeBuilder {
120 runtime: RenderRuntime,
121}
122
123impl RenderRuntimeBuilder {
124 pub fn new() -> Self {
126 Self::default()
127 }
128
129 pub fn with_stdout_is_tty(mut self, stdout_is_tty: bool) -> Self {
131 self.runtime.stdout_is_tty = stdout_is_tty;
132 self
133 }
134
135 pub fn with_terminal(mut self, terminal: impl Into<String>) -> Self {
137 self.runtime.terminal = Some(terminal.into());
138 self
139 }
140
141 pub fn with_no_color(mut self, no_color: bool) -> Self {
143 self.runtime.no_color = no_color;
144 self
145 }
146
147 pub fn with_width(mut self, width: usize) -> Self {
149 self.runtime.width = Some(width);
150 self
151 }
152
153 pub fn with_locale_utf8(mut self, locale_utf8: bool) -> Self {
155 self.runtime.locale_utf8 = Some(locale_utf8);
156 self
157 }
158
159 pub fn build(self) -> RenderRuntime {
161 self.runtime
162 }
163}
164
165#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
167pub struct HelpChromeSettings {
168 pub table_chrome: HelpTableChrome,
170 pub entry_indent: Option<usize>,
172 pub entry_gap: Option<usize>,
174 pub section_spacing: Option<usize>,
176}
177
178#[derive(Debug, Clone)]
180#[non_exhaustive]
181pub struct RenderSettings {
182 pub format: OutputFormat,
184 pub format_explicit: bool,
186 pub mode: RenderMode,
188 pub color: ColorMode,
190 pub unicode: UnicodeMode,
192 pub width: Option<usize>,
194 pub margin: usize,
196 pub indent_size: usize,
198 pub short_list_max: usize,
200 pub medium_list_max: usize,
202 pub grid_padding: usize,
204 pub grid_columns: Option<usize>,
206 pub column_weight: usize,
208 pub table_overflow: TableOverflow,
210 pub table_border: TableBorderStyle,
212 pub help_chrome: HelpChromeSettings,
214 pub mreg_stack_min_col_width: usize,
216 pub mreg_stack_overflow_ratio: usize,
218 pub theme_name: String,
220 pub(crate) theme: Option<ThemeDefinition>,
225 pub style_overrides: StyleOverrides,
227 pub chrome_frame: SectionFrameStyle,
229 pub ruled_section_policy: RuledSectionPolicy,
231 pub guide_default_format: GuideDefaultFormat,
233 pub runtime: RenderRuntime,
235}
236
237impl Default for RenderSettings {
238 fn default() -> Self {
239 Self {
240 format: OutputFormat::Auto,
241 format_explicit: false,
242 mode: RenderMode::Auto,
243 color: ColorMode::Auto,
244 unicode: UnicodeMode::Auto,
245 width: None,
246 margin: 0,
247 indent_size: 2,
248 short_list_max: 1,
249 medium_list_max: 5,
250 grid_padding: 4,
251 grid_columns: None,
252 column_weight: 3,
253 table_overflow: TableOverflow::Clip,
254 table_border: TableBorderStyle::Square,
255 help_chrome: HelpChromeSettings::default(),
256 mreg_stack_min_col_width: 10,
257 mreg_stack_overflow_ratio: 200,
258 theme_name: crate::ui::theme::DEFAULT_THEME_NAME.to_string(),
259 theme: None,
260 style_overrides: crate::ui::style::StyleOverrides::default(),
261 chrome_frame: SectionFrameStyle::Top,
262 ruled_section_policy: RuledSectionPolicy::PerSection,
263 guide_default_format: GuideDefaultFormat::Guide,
264 runtime: RenderRuntime::default(),
265 }
266 }
267}
268
269#[derive(Debug, Clone, Default)]
271pub struct RenderSettingsBuilder {
272 settings: RenderSettings,
273}
274
275impl RenderSettingsBuilder {
276 pub fn new() -> Self {
278 Self::default()
279 }
280
281 pub fn plain(format: OutputFormat) -> Self {
283 Self {
284 settings: RenderSettings {
285 format,
286 format_explicit: false,
287 mode: RenderMode::Plain,
288 color: ColorMode::Never,
289 unicode: UnicodeMode::Never,
290 ..RenderSettings::default()
291 },
292 }
293 }
294
295 pub fn with_format(mut self, format: OutputFormat) -> Self {
297 self.settings.format = format;
298 self
299 }
300
301 pub fn with_format_explicit(mut self, format_explicit: bool) -> Self {
303 self.settings.format_explicit = format_explicit;
304 self
305 }
306
307 pub fn with_mode(mut self, mode: RenderMode) -> Self {
309 self.settings.mode = mode;
310 self
311 }
312
313 pub fn with_color(mut self, color: ColorMode) -> Self {
315 self.settings.color = color;
316 self
317 }
318
319 pub fn with_unicode(mut self, unicode: UnicodeMode) -> Self {
321 self.settings.unicode = unicode;
322 self
323 }
324
325 pub fn with_width(mut self, width: usize) -> Self {
327 self.settings.width = Some(width);
328 self
329 }
330
331 pub fn with_margin(mut self, margin: usize) -> Self {
333 self.settings.margin = margin;
334 self
335 }
336
337 pub fn with_indent_size(mut self, indent_size: usize) -> Self {
339 self.settings.indent_size = indent_size;
340 self
341 }
342
343 pub fn with_table_overflow(mut self, table_overflow: TableOverflow) -> Self {
345 self.settings.table_overflow = table_overflow;
346 self
347 }
348
349 pub fn with_table_border(mut self, table_border: TableBorderStyle) -> Self {
351 self.settings.table_border = table_border;
352 self
353 }
354
355 pub fn with_help_chrome(mut self, help_chrome: HelpChromeSettings) -> Self {
357 self.settings.help_chrome = help_chrome;
358 self
359 }
360
361 pub fn with_theme_name(mut self, theme_name: impl Into<String>) -> Self {
363 self.settings.theme_name = theme_name.into();
364 self
365 }
366
367 pub fn with_style_overrides(mut self, style_overrides: StyleOverrides) -> Self {
369 self.settings.style_overrides = style_overrides;
370 self
371 }
372
373 pub fn with_chrome_frame(mut self, chrome_frame: SectionFrameStyle) -> Self {
375 self.settings.chrome_frame = chrome_frame;
376 self
377 }
378
379 pub fn with_ruled_section_policy(mut self, ruled_section_policy: RuledSectionPolicy) -> Self {
381 self.settings.ruled_section_policy = ruled_section_policy;
382 self
383 }
384
385 pub fn with_guide_default_format(mut self, guide_default_format: GuideDefaultFormat) -> Self {
387 self.settings.guide_default_format = guide_default_format;
388 self
389 }
390
391 pub fn with_runtime(mut self, runtime: RenderRuntime) -> Self {
393 self.settings.runtime = runtime;
394 self
395 }
396
397 pub fn build(self) -> RenderSettings {
399 self.settings
400 }
401}
402
403#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
405pub enum GuideDefaultFormat {
406 #[default]
408 Guide,
409 Inherit,
411}
412
413impl GuideDefaultFormat {
414 pub fn parse(value: &str) -> Option<Self> {
426 match value.trim().to_ascii_lowercase().as_str() {
427 "guide" => Some(Self::Guide),
428 "inherit" | "none" => Some(Self::Inherit),
429 _ => None,
430 }
431 }
432}
433
434#[derive(Debug, Clone, Copy, PartialEq, Eq)]
436pub enum RenderBackend {
437 Plain,
439 Rich,
441}
442
443#[derive(Debug, Clone, Copy, PartialEq, Eq)]
445pub enum TableOverflow {
446 None,
448 Clip,
450 Ellipsis,
452 Wrap,
454}
455
456impl TableOverflow {
457 pub fn parse(value: &str) -> Option<Self> {
469 match value.trim().to_ascii_lowercase().as_str() {
470 "none" | "visible" => Some(Self::None),
471 "clip" | "hidden" | "crop" => Some(Self::Clip),
472 "ellipsis" | "truncate" => Some(Self::Ellipsis),
473 "wrap" | "wrapped" => Some(Self::Wrap),
474 _ => None,
475 }
476 }
477}
478
479#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
481pub enum TableBorderStyle {
482 None,
484 #[default]
486 Square,
487 Round,
489}
490
491impl TableBorderStyle {
492 pub fn parse(value: &str) -> Option<Self> {
504 match value.trim().to_ascii_lowercase().as_str() {
505 "none" | "plain" => Some(Self::None),
506 "square" | "box" | "boxed" => Some(Self::Square),
507 "round" | "rounded" => Some(Self::Round),
508 _ => None,
509 }
510 }
511}
512
513#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
515pub enum HelpTableChrome {
516 Inherit,
518 #[default]
520 None,
521 Square,
523 Round,
525}
526
527impl HelpTableChrome {
528 pub fn parse(value: &str) -> Option<Self> {
540 match value.trim().to_ascii_lowercase().as_str() {
541 "inherit" => Some(Self::Inherit),
542 "none" | "plain" => Some(Self::None),
543 "square" | "box" | "boxed" => Some(Self::Square),
544 "round" | "rounded" => Some(Self::Round),
545 _ => None,
546 }
547 }
548
549 pub fn resolve(self, table_border: TableBorderStyle) -> TableBorderStyle {
566 match self {
567 Self::Inherit => table_border,
568 Self::None => TableBorderStyle::None,
569 Self::Square => TableBorderStyle::Square,
570 Self::Round => TableBorderStyle::Round,
571 }
572 }
573}
574
575impl RenderSettings {
576 pub fn builder() -> RenderSettingsBuilder {
578 RenderSettingsBuilder::new()
579 }
580
581 pub fn test_plain(format: OutputFormat) -> Self {
600 RenderSettingsBuilder::plain(format).build()
601 }
602
603 pub fn prefers_guide_rendering(&self) -> bool {
625 matches!(self.format, OutputFormat::Guide)
626 || (!self.format_explicit
627 && matches!(self.guide_default_format, GuideDefaultFormat::Guide))
628 }
629}
630
631pub fn render_rows(rows: &[Row], settings: &RenderSettings) -> String {
633 render_output(
634 &OutputResult {
635 items: OutputItems::Rows(rows.to_vec()),
636 document: None,
637 meta: Default::default(),
638 },
639 settings,
640 )
641}
642
643pub fn render_output(output: &OutputResult, settings: &RenderSettings) -> String {
645 let plan = settings.resolve_render_plan(output);
646 if matches!(plan.format, OutputFormat::Markdown)
647 && let Some(guide) = GuideView::try_from_output_result(output)
648 {
649 return guide.to_markdown_with_width(plan.render.width);
650 }
651 let document = format::build_document_from_output_plan(output, &plan);
652 renderer::render_document(&document, plan.render)
653}
654
655fn render_guide_document(document: &Document, settings: &RenderSettings) -> String {
656 let mut rendered = render_document_resolved(document, settings.resolve_render_settings());
657 if !rendered.ends_with('\n') {
658 rendered.push('\n');
659 }
660 rendered
661}
662
663pub(crate) fn render_guide_view_with_options(
664 guide: &GuideView,
665 settings: &RenderSettings,
666 options: crate::ui::format::help::GuideRenderOptions<'_>,
667) -> String {
668 if matches!(
669 format::resolve_output_format(&guide.to_output_result(), settings),
670 OutputFormat::Guide
671 ) {
672 let document = crate::ui::format::help::build_guide_document_from_view(guide, options);
673 return render_guide_document(&document, settings);
674 }
675
676 render_output(&guide.to_output_result(), settings)
677}
678
679pub(crate) fn render_guide_payload(
680 config: &crate::config::ResolvedConfig,
681 settings: &RenderSettings,
682 guide: &GuideView,
683) -> String {
684 render_guide_payload_with_layout(
685 guide,
686 settings,
687 crate::ui::presentation::help_layout(config),
688 )
689}
690
691pub(crate) fn render_guide_payload_with_layout(
692 guide: &GuideView,
693 settings: &RenderSettings,
694 layout: crate::ui::presentation::HelpLayout,
695) -> String {
696 let guide_settings = settings.resolve_guide_render_settings();
697 render_guide_view_with_options(
698 guide,
699 settings,
700 crate::ui::format::help::GuideRenderOptions {
701 title_prefix: None,
702 layout,
703 guide: guide_settings,
704 panel_kind: None,
705 },
706 )
707}
708
709pub(crate) fn render_guide_output_with_options(
710 output: &OutputResult,
711 settings: &RenderSettings,
712 options: crate::ui::format::help::GuideRenderOptions<'_>,
713) -> String {
714 if matches!(
715 format::resolve_output_format(output, settings),
716 OutputFormat::Guide
717 ) && let Some(guide) = GuideView::try_from_output_result(output)
718 {
719 return render_guide_view_with_options(&guide, settings, options);
720 }
721
722 render_output(output, settings)
723}
724
725pub(crate) fn guide_render_options<'a>(
726 config: &'a crate::config::ResolvedConfig,
727 settings: &'a RenderSettings,
728) -> crate::ui::format::help::GuideRenderOptions<'a> {
729 let guide_settings = settings.resolve_guide_render_settings();
730 crate::ui::format::help::GuideRenderOptions {
731 title_prefix: None,
732 layout: crate::ui::presentation::help_layout(config),
733 guide: guide_settings,
734 panel_kind: None,
735 }
736}
737
738pub(crate) fn render_structured_output(
739 config: &crate::config::ResolvedConfig,
740 settings: &RenderSettings,
741 output: &OutputResult,
742) -> String {
743 if GuideView::try_from_output_result(output).is_some() {
744 return render_guide_output_with_options(
745 output,
746 settings,
747 guide_render_options(config, settings),
748 );
749 }
750 render_output(output, settings)
751}
752
753pub fn render_document(document: &Document, settings: &RenderSettings) -> String {
755 let resolved = settings.resolve_render_settings();
756 renderer::render_document(document, resolved)
757}
758
759pub(crate) fn render_document_resolved(
760 document: &Document,
761 settings: ResolvedRenderSettings,
762) -> String {
763 renderer::render_document(document, settings)
764}
765
766pub fn render_rows_for_copy(rows: &[Row], settings: &RenderSettings) -> String {
786 render_output_for_copy(
787 &OutputResult {
788 items: OutputItems::Rows(rows.to_vec()),
789 document: None,
790 meta: Default::default(),
791 },
792 settings,
793 )
794}
795
796pub fn render_output_for_copy(output: &OutputResult, settings: &RenderSettings) -> String {
798 let copy_settings = settings.plain_copy_settings();
799 let plan = copy_settings.resolve_render_plan(output);
800 if matches!(plan.format, OutputFormat::Markdown)
801 && let Some(guide) = GuideView::try_from_output_result(output)
802 {
803 return guide.to_markdown_with_width(plan.render.width);
804 }
805 let document = format::build_document_from_output_plan(output, &plan);
806 renderer::render_document(&document, plan.render)
807}
808
809pub fn render_document_for_copy(document: &Document, settings: &RenderSettings) -> String {
811 let copy_settings = settings.plain_copy_settings();
812 let resolved = copy_settings.resolve_render_settings();
813 renderer::render_document(document, resolved)
814}
815
816pub fn copy_rows_to_clipboard(
818 rows: &[Row],
819 settings: &RenderSettings,
820 clipboard: &clipboard::ClipboardService,
821) -> Result<(), clipboard::ClipboardError> {
822 copy_output_to_clipboard(
823 &OutputResult {
824 items: OutputItems::Rows(rows.to_vec()),
825 document: None,
826 meta: Default::default(),
827 },
828 settings,
829 clipboard,
830 )
831}
832
833pub fn copy_output_to_clipboard(
835 output: &OutputResult,
836 settings: &RenderSettings,
837 clipboard: &clipboard::ClipboardService,
838) -> Result<(), clipboard::ClipboardError> {
839 let text = render_output_for_copy(output, settings);
840 clipboard.copy_text(&text)
841}
842
843#[cfg(test)]
844mod tests {
845 use super::{
846 GuideDefaultFormat, HelpChromeSettings, HelpTableChrome, RenderBackend, RenderRuntime,
847 RenderSettings, RenderSettingsBuilder, TableBorderStyle, TableOverflow, format,
848 render_document, render_document_for_copy, render_output, render_output_for_copy,
849 render_rows, render_rows_for_copy,
850 };
851 use crate::core::output::{ColorMode, OutputFormat, RenderMode, UnicodeMode};
852 use crate::core::output_model::OutputResult;
853 use crate::core::row::Row;
854 use crate::guide::GuideView;
855 use crate::ui::document::{Block, MregValue, TableStyle};
856 use serde_json::json;
857
858 fn settings(format: OutputFormat) -> RenderSettings {
859 RenderSettings {
860 mode: RenderMode::Auto,
861 ..RenderSettings::test_plain(format)
862 }
863 }
864
865 #[test]
866 fn document_builder_selects_auto_and_explicit_block_shapes_unit() {
867 let value_rows = vec![{
868 let mut row = Row::new();
869 row.insert("value".to_string(), json!("hello"));
870 row
871 }];
872 let document = format::build_document(&value_rows, &settings(OutputFormat::Auto));
873 assert!(matches!(document.blocks[0], Block::Value(_)));
874
875 let mreg_rows = vec![{
876 let mut row = Row::new();
877 row.insert("uid".to_string(), json!("oistes"));
878 row
879 }];
880 let document = format::build_document(&mreg_rows, &settings(OutputFormat::Auto));
881 assert!(matches!(document.blocks[0], Block::Mreg(_)));
882
883 let table_rows = vec![
884 {
885 let mut row = Row::new();
886 row.insert("uid".to_string(), json!("one"));
887 row
888 },
889 {
890 let mut row = Row::new();
891 row.insert("uid".to_string(), json!("two"));
892 row
893 },
894 ];
895 let document = format::build_document(&table_rows, &settings(OutputFormat::Auto));
896 assert!(matches!(document.blocks[0], Block::Table(_)));
897
898 let rich_rows = vec![{
899 let mut row = Row::new();
900 row.insert("uid".to_string(), json!("oistes"));
901 row.insert("groups".to_string(), json!(["a", "b"]));
902 row
903 }];
904 let document = format::build_document(&rich_rows, &settings(OutputFormat::Mreg));
905 let Block::Mreg(block) = &document.blocks[0] else {
906 panic!("expected mreg block");
907 };
908 assert_eq!(block.rows.len(), 1);
909 assert!(
910 block.rows[0]
911 .entries
912 .iter()
913 .any(|entry| matches!(entry.value, MregValue::Scalar(_)))
914 );
915 assert!(
916 block.rows[0]
917 .entries
918 .iter()
919 .any(|entry| matches!(entry.value, MregValue::VerticalList(_)))
920 );
921
922 let markdown_rows = vec![{
923 let mut row = Row::new();
924 row.insert("uid".to_string(), json!("oistes"));
925 row
926 }];
927 let document = format::build_document(&markdown_rows, &settings(OutputFormat::Markdown));
928 let Block::Table(table) = &document.blocks[0] else {
929 panic!("expected table block");
930 };
931 assert_eq!(table.style, TableStyle::Markdown);
932 }
933
934 #[test]
935 fn semantic_guide_markdown_output_and_copy_remain_section_based_unit() {
936 let output =
937 GuideView::from_text("Usage: osp history <COMMAND>\n\nCommands:\n list Show\n")
938 .to_output_result();
939 let settings = RenderSettings {
940 format: OutputFormat::Markdown,
941 format_explicit: true,
942 ..settings(OutputFormat::Markdown)
943 };
944
945 let rendered = render_output(&output, &settings);
946 let copied = render_output_for_copy(&output, &settings);
947
948 for text in [&rendered, &copied] {
949 assert!(text.contains("## Usage"));
950 assert!(text.contains("## Commands"));
951 assert!(text.contains("- `list` Show"));
952 assert!(!text.contains("| name"));
953 }
954 assert!(!copied.contains("\x1b["));
955 }
956
957 #[test]
958 fn render_builders_and_parse_helpers_cover_configuration_surface_unit() {
959 let runtime = RenderRuntime::builder()
960 .with_stdout_is_tty(true)
961 .with_terminal("xterm-256color")
962 .with_no_color(true)
963 .with_width(98)
964 .with_locale_utf8(false)
965 .build();
966 assert_eq!(
967 runtime,
968 RenderRuntime {
969 stdout_is_tty: true,
970 terminal: Some("xterm-256color".to_string()),
971 no_color: true,
972 width: Some(98),
973 locale_utf8: Some(false),
974 }
975 );
976
977 let settings = RenderSettings::builder()
978 .with_format(OutputFormat::Markdown)
979 .with_format_explicit(true)
980 .with_mode(RenderMode::Rich)
981 .with_color(ColorMode::Always)
982 .with_unicode(UnicodeMode::Auto)
983 .with_width(98)
984 .with_margin(2)
985 .with_indent_size(4)
986 .with_table_overflow(TableOverflow::Wrap)
987 .with_table_border(TableBorderStyle::Round)
988 .with_help_chrome(HelpChromeSettings {
989 table_chrome: HelpTableChrome::Inherit,
990 ..HelpChromeSettings::default()
991 })
992 .with_theme_name("dracula")
993 .with_style_overrides(Default::default())
994 .with_chrome_frame(crate::ui::SectionFrameStyle::Round)
995 .with_guide_default_format(GuideDefaultFormat::Inherit)
996 .with_runtime(runtime.clone())
997 .build();
998 assert_eq!(settings.format, OutputFormat::Markdown);
999 assert!(settings.format_explicit);
1000 assert_eq!(settings.mode, RenderMode::Rich);
1001 assert_eq!(settings.color, ColorMode::Always);
1002 assert_eq!(settings.unicode, UnicodeMode::Auto);
1003 assert_eq!(settings.width, Some(98));
1004 assert_eq!(settings.margin, 2);
1005 assert_eq!(settings.indent_size, 4);
1006 assert_eq!(settings.table_overflow, TableOverflow::Wrap);
1007 assert_eq!(settings.table_border, TableBorderStyle::Round);
1008 assert_eq!(settings.help_chrome.table_chrome, HelpTableChrome::Inherit);
1009 assert_eq!(settings.theme_name, "dracula");
1010 assert_eq!(settings.chrome_frame, crate::ui::SectionFrameStyle::Round);
1011 assert_eq!(settings.guide_default_format, GuideDefaultFormat::Inherit);
1012 assert_eq!(settings.runtime, runtime);
1013
1014 let plain = RenderSettingsBuilder::plain(OutputFormat::Json).build();
1015 assert_eq!(plain.mode, RenderMode::Plain);
1016 assert_eq!(plain.color, ColorMode::Never);
1017 assert_eq!(plain.unicode, UnicodeMode::Never);
1018
1019 assert_eq!(
1020 GuideDefaultFormat::parse("none"),
1021 Some(GuideDefaultFormat::Inherit)
1022 );
1023 assert_eq!(GuideDefaultFormat::parse("wat"), None);
1024 assert_eq!(
1025 HelpTableChrome::parse("round"),
1026 Some(HelpTableChrome::Round)
1027 );
1028 assert_eq!(HelpTableChrome::parse("wat"), None);
1029 assert_eq!(
1030 HelpTableChrome::Inherit.resolve(TableBorderStyle::Round),
1031 TableBorderStyle::Round
1032 );
1033 assert_eq!(
1034 HelpTableChrome::None.resolve(TableBorderStyle::Square),
1035 TableBorderStyle::None
1036 );
1037 assert_eq!(
1038 HelpTableChrome::Square.resolve(TableBorderStyle::None),
1039 TableBorderStyle::Square
1040 );
1041 assert_eq!(
1042 TableBorderStyle::parse("none"),
1043 Some(TableBorderStyle::None)
1044 );
1045 assert_eq!(
1046 TableBorderStyle::parse("box"),
1047 Some(TableBorderStyle::Square)
1048 );
1049 assert_eq!(
1050 TableBorderStyle::parse("square"),
1051 Some(TableBorderStyle::Square)
1052 );
1053 assert_eq!(
1054 TableBorderStyle::parse("round"),
1055 Some(TableBorderStyle::Round)
1056 );
1057 assert_eq!(
1058 TableBorderStyle::parse("rounded"),
1059 Some(TableBorderStyle::Round)
1060 );
1061 assert_eq!(TableBorderStyle::parse("mystery"), None);
1062 assert_eq!(TableOverflow::parse("visible"), Some(TableOverflow::None));
1063 assert_eq!(TableOverflow::parse("crop"), Some(TableOverflow::Clip));
1064 assert_eq!(
1065 TableOverflow::parse("truncate"),
1066 Some(TableOverflow::Ellipsis)
1067 );
1068 assert_eq!(TableOverflow::parse("wrapped"), Some(TableOverflow::Wrap));
1069 assert_eq!(TableOverflow::parse("other"), None);
1070 }
1071
1072 #[test]
1073 fn render_resolution_covers_public_helpers_mode_runtime_and_force_rules_unit() {
1074 let rows = vec![{
1075 let mut row = Row::new();
1076 row.insert("uid".to_string(), json!("alice"));
1077 row
1078 }];
1079 let rendered = render_rows(&rows, &settings(OutputFormat::Table));
1080 assert!(rendered.contains("uid"));
1081 assert!(rendered.contains("alice"));
1082
1083 let dumb_terminal = RenderSettings {
1084 mode: RenderMode::Rich,
1085 color: ColorMode::Auto,
1086 unicode: UnicodeMode::Auto,
1087 width: Some(0),
1088 grid_columns: Some(0),
1089 runtime: RenderRuntime {
1090 stdout_is_tty: true,
1091 terminal: Some("dumb".to_string()),
1092 no_color: false,
1093 width: Some(0),
1094 locale_utf8: Some(true),
1095 },
1096 ..RenderSettings::test_plain(OutputFormat::Table)
1097 };
1098 let dumb_resolved = dumb_terminal.resolve_render_settings();
1099 assert_eq!(dumb_resolved.backend, RenderBackend::Rich);
1100 assert!(dumb_resolved.color);
1101 assert!(!dumb_resolved.unicode);
1102 assert_eq!(dumb_resolved.width, None);
1103 assert_eq!(dumb_resolved.grid_columns, None);
1104
1105 let locale_false = RenderSettings {
1106 mode: RenderMode::Rich,
1107 color: ColorMode::Auto,
1108 unicode: UnicodeMode::Auto,
1109 runtime: RenderRuntime {
1110 stdout_is_tty: true,
1111 terminal: Some("xterm-256color".to_string()),
1112 no_color: false,
1113 width: Some(72),
1114 locale_utf8: Some(false),
1115 },
1116 ..RenderSettings::test_plain(OutputFormat::Table)
1117 };
1118 let locale_resolved = locale_false.resolve_render_settings();
1119 assert!(locale_resolved.color);
1120 assert!(!locale_resolved.unicode);
1121 assert_eq!(locale_resolved.width, Some(72));
1122
1123 let plain = RenderSettings {
1124 format: OutputFormat::Table,
1125 color: ColorMode::Always,
1126 unicode: UnicodeMode::Always,
1127 ..RenderSettings::test_plain(OutputFormat::Table)
1128 };
1129 let resolved = plain.resolve_render_settings();
1130 assert_eq!(resolved.backend, RenderBackend::Plain);
1131 assert!(!resolved.color);
1132 assert!(!resolved.unicode);
1133
1134 let rich = RenderSettings {
1135 format: OutputFormat::Table,
1136 mode: RenderMode::Rich,
1137 color: ColorMode::Always,
1138 unicode: UnicodeMode::Always,
1139 ..RenderSettings::test_plain(OutputFormat::Table)
1140 };
1141 let resolved = rich.resolve_render_settings();
1142 assert_eq!(resolved.backend, RenderBackend::Rich);
1143 assert!(resolved.color);
1144 assert!(resolved.unicode);
1145 let auto = RenderSettings {
1146 mode: RenderMode::Auto,
1147 color: ColorMode::Auto,
1148 unicode: UnicodeMode::Auto,
1149 runtime: super::RenderRuntime {
1150 stdout_is_tty: true,
1151 terminal: Some("dumb".to_string()),
1152 no_color: false,
1153 width: Some(72),
1154 locale_utf8: Some(false),
1155 },
1156 ..RenderSettings::test_plain(OutputFormat::Table)
1157 };
1158 let resolved = auto.resolve_render_settings();
1159 assert_eq!(resolved.backend, RenderBackend::Plain);
1160 assert!(!resolved.color);
1161 assert!(!resolved.unicode);
1162 assert_eq!(resolved.width, Some(72));
1163
1164 let forced_color = RenderSettings {
1165 mode: RenderMode::Auto,
1166 color: ColorMode::Always,
1167 unicode: UnicodeMode::Auto,
1168 runtime: super::RenderRuntime {
1169 stdout_is_tty: false,
1170 terminal: Some("xterm-256color".to_string()),
1171 no_color: false,
1172 width: Some(80),
1173 locale_utf8: Some(true),
1174 },
1175 ..RenderSettings::test_plain(OutputFormat::Table)
1176 };
1177 let resolved = forced_color.resolve_render_settings();
1178 assert_eq!(resolved.backend, RenderBackend::Rich);
1179 assert!(resolved.color);
1180
1181 let forced_unicode = RenderSettings {
1182 mode: RenderMode::Auto,
1183 color: ColorMode::Auto,
1184 unicode: UnicodeMode::Always,
1185 runtime: super::RenderRuntime {
1186 stdout_is_tty: false,
1187 terminal: Some("dumb".to_string()),
1188 no_color: true,
1189 width: Some(64),
1190 locale_utf8: Some(false),
1191 },
1192 ..RenderSettings::test_plain(OutputFormat::Table)
1193 };
1194 let resolved = forced_unicode.resolve_render_settings();
1195 assert_eq!(resolved.backend, RenderBackend::Rich);
1196 assert!(!resolved.color);
1197 assert!(resolved.unicode);
1198
1199 let guide_settings = RenderSettings {
1200 help_chrome: HelpChromeSettings {
1201 table_chrome: HelpTableChrome::Inherit,
1202 entry_indent: Some(4),
1203 entry_gap: Some(3),
1204 section_spacing: Some(0),
1205 },
1206 table_border: TableBorderStyle::Round,
1207 chrome_frame: crate::ui::SectionFrameStyle::TopBottom,
1208 ..RenderSettings::test_plain(OutputFormat::Guide)
1209 };
1210 let guide_resolved = guide_settings.resolve_guide_render_settings();
1211 assert_eq!(
1212 guide_resolved.frame_style,
1213 crate::ui::SectionFrameStyle::TopBottom
1214 );
1215 assert_eq!(
1216 guide_resolved.help_chrome.table_border,
1217 TableBorderStyle::Round
1218 );
1219 assert_eq!(guide_resolved.help_chrome.entry_indent, Some(4));
1220 assert_eq!(guide_resolved.help_chrome.entry_gap, Some(3));
1221 assert_eq!(guide_resolved.help_chrome.section_spacing, Some(0));
1222
1223 let mreg_settings = RenderSettings {
1224 short_list_max: 0,
1225 medium_list_max: 0,
1226 indent_size: 0,
1227 mreg_stack_min_col_width: 0,
1228 mreg_stack_overflow_ratio: 10,
1229 ..RenderSettings::test_plain(OutputFormat::Mreg)
1230 };
1231 let mreg_resolved = mreg_settings.resolve_mreg_build_settings();
1232 assert_eq!(mreg_resolved.short_list_max, 1);
1233 assert_eq!(mreg_resolved.medium_list_max, 2);
1234 assert_eq!(mreg_resolved.indent_size, 1);
1235 assert_eq!(mreg_resolved.stack_min_col_width, 1);
1236 assert_eq!(mreg_resolved.stack_overflow_ratio, 100);
1237 }
1238
1239 #[test]
1240 fn copy_helpers_force_plain_copy_mode_for_rows_documents_and_json_unit() {
1241 let table_rows = vec![{
1242 let mut row = Row::new();
1243 row.insert("uid".to_string(), json!("oistes"));
1244 row.insert(
1245 "description".to_string(),
1246 json!("very long text that will be shown"),
1247 );
1248 row
1249 }];
1250 let rich_table = RenderSettings {
1251 format: OutputFormat::Table,
1252 mode: RenderMode::Rich,
1253 color: ColorMode::Always,
1254 unicode: UnicodeMode::Always,
1255 ..RenderSettings::test_plain(OutputFormat::Table)
1256 };
1257 let table_copy = render_rows_for_copy(&table_rows, &rich_table);
1258 assert!(!table_copy.contains("\x1b["));
1259 assert!(!table_copy.contains('┌'));
1260 assert!(table_copy.contains('+'));
1261
1262 let value_rows = vec![{
1263 let mut row = Row::new();
1264 row.insert("value".to_string(), json!("hello"));
1265 row
1266 }];
1267 let rich_value = RenderSettings {
1268 mode: RenderMode::Rich,
1269 color: ColorMode::Always,
1270 unicode: UnicodeMode::Always,
1271 ..RenderSettings::test_plain(OutputFormat::Value)
1272 };
1273 let value_copy = render_rows_for_copy(&value_rows, &rich_value);
1274 assert_eq!(value_copy.trim(), "hello");
1275 assert!(!value_copy.contains("\x1b["));
1276
1277 let document = crate::ui::Document {
1278 blocks: vec![Block::Line(crate::ui::LineBlock {
1279 parts: vec![crate::ui::LinePart {
1280 text: "hello".to_string(),
1281 token: None,
1282 }],
1283 })],
1284 };
1285 let rich_document = RenderSettings {
1286 mode: RenderMode::Rich,
1287 color: ColorMode::Always,
1288 unicode: UnicodeMode::Always,
1289 ..RenderSettings::test_plain(OutputFormat::Table)
1290 };
1291 let rich = render_document(&document, &rich_document);
1292 let copied = render_document_for_copy(&document, &rich_document);
1293
1294 assert!(rich.contains("hello"));
1295 assert!(copied.contains("hello"));
1296 assert!(!copied.contains("\x1b["));
1297
1298 let json_rows = vec![{
1299 let mut row = Row::new();
1300 row.insert("uid".to_string(), json!("alice"));
1301 row.insert("count".to_string(), json!(2));
1302 row
1303 }];
1304 let json_settings = RenderSettings {
1305 format: OutputFormat::Json,
1306 mode: RenderMode::Rich,
1307 color: ColorMode::Always,
1308 unicode: UnicodeMode::Always,
1309 ..RenderSettings::test_plain(OutputFormat::Json)
1310 };
1311
1312 let output = OutputResult::from_rows(json_rows);
1313 let rendered = render_output(&output, &json_settings);
1314 let copied = render_output_for_copy(&output, &json_settings);
1315
1316 assert!(rendered.contains("\"uid\""));
1317 assert!(rendered.contains("\x1b["));
1318 assert_eq!(
1319 copied,
1320 "[\n {\n \"uid\": \"alice\",\n \"count\": 2\n }\n]\n"
1321 );
1322 assert!(!copied.contains("\x1b["));
1323 }
1324}