1pub mod chrome;
94pub mod clipboard;
96mod display;
97pub mod document;
98pub(crate) mod document_model;
99pub(crate) mod format;
100pub mod inline;
102pub mod interactive;
103mod layout;
104pub mod messages;
105pub(crate) mod presentation;
106mod renderer;
107mod resolution;
108pub mod style;
110pub mod theme;
112pub(crate) mod theme_loader;
113mod width;
114
115use crate::core::output::{ColorMode, OutputFormat, RenderMode, UnicodeMode};
116use crate::core::output_model::{OutputItems, OutputResult};
117use crate::core::row::Row;
118use crate::guide::GuideView;
119
120pub use chrome::{
121 RuledSectionPolicy, SectionFrameStyle, SectionRenderContext, SectionStyleTokens,
122 render_section_block_with_overrides, render_section_divider_with_overrides,
123};
124pub use clipboard::{ClipboardError, ClipboardService};
125pub use document::{
126 Block, CodeBlock, Document, JsonBlock, LineBlock, LinePart, MregBlock, MregEntry, MregRow,
127 MregValue, PanelBlock, PanelRules, TableAlign, TableBlock, TableStyle, ValueBlock,
128};
129pub use inline::{line_from_inline, parts_from_inline, render_inline};
130pub use interactive::{Interactive, InteractiveResult, InteractiveRuntime, Spinner};
131pub use messages::{
132 GroupedRenderOptions, MessageBuffer, MessageLayout, MessageLevel, UiMessage, adjust_verbosity,
133};
134pub(crate) use resolution::ResolvedGuideRenderSettings;
135#[cfg(test)]
136pub(crate) use resolution::ResolvedHelpChromeSettings;
137pub(crate) use resolution::ResolvedRenderPlan;
138pub use resolution::ResolvedRenderSettings;
139pub use style::{StyleOverrides, StyleToken};
140pub use theme::{
141 DEFAULT_THEME_NAME, ThemeDefinition, ThemeOverrides, ThemePalette, all_themes,
142 available_theme_names, builtin_themes, display_name_from_id, find_builtin_theme, find_theme,
143 is_known_theme, normalize_theme_name, resolve_theme,
144};
145
146#[derive(Debug, Clone, Default, PartialEq, Eq)]
148#[non_exhaustive]
149#[must_use]
150pub struct RenderRuntime {
151 pub stdout_is_tty: bool,
153 pub terminal: Option<String>,
155 pub no_color: bool,
157 pub width: Option<usize>,
159 pub locale_utf8: Option<bool>,
161}
162
163impl RenderRuntime {
164 pub fn builder() -> RenderRuntimeBuilder {
185 RenderRuntimeBuilder::new()
186 }
187}
188
189#[derive(Debug, Clone, Default)]
196#[must_use]
197pub struct RenderRuntimeBuilder {
198 runtime: RenderRuntime,
199}
200
201impl RenderRuntimeBuilder {
202 pub fn new() -> Self {
204 Self::default()
205 }
206
207 pub fn with_stdout_is_tty(mut self, stdout_is_tty: bool) -> Self {
209 self.runtime.stdout_is_tty = stdout_is_tty;
210 self
211 }
212
213 pub fn with_terminal(mut self, terminal: impl Into<String>) -> Self {
215 self.runtime.terminal = Some(terminal.into());
216 self
217 }
218
219 pub fn with_no_color(mut self, no_color: bool) -> Self {
221 self.runtime.no_color = no_color;
222 self
223 }
224
225 pub fn with_width(mut self, width: usize) -> Self {
227 self.runtime.width = Some(width);
228 self
229 }
230
231 pub fn with_locale_utf8(mut self, locale_utf8: bool) -> Self {
233 self.runtime.locale_utf8 = Some(locale_utf8);
234 self
235 }
236
237 pub fn build(self) -> RenderRuntime {
239 self.runtime
240 }
241}
242
243#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
245pub struct HelpChromeSettings {
246 pub table_chrome: HelpTableChrome,
248 pub entry_indent: Option<usize>,
250 pub entry_gap: Option<usize>,
252 pub section_spacing: Option<usize>,
254}
255
256#[derive(Debug, Clone)]
264#[non_exhaustive]
265#[must_use]
266pub struct RenderSettings {
267 pub format: OutputFormat,
269 pub format_explicit: bool,
271 pub mode: RenderMode,
273 pub color: ColorMode,
275 pub unicode: UnicodeMode,
277 pub width: Option<usize>,
279 pub margin: usize,
281 pub indent_size: usize,
283 pub short_list_max: usize,
285 pub medium_list_max: usize,
287 pub grid_padding: usize,
289 pub grid_columns: Option<usize>,
291 pub column_weight: usize,
293 pub table_overflow: TableOverflow,
295 pub table_border: TableBorderStyle,
297 pub help_chrome: HelpChromeSettings,
299 pub mreg_stack_min_col_width: usize,
301 pub mreg_stack_overflow_ratio: usize,
303 pub theme_name: String,
305 pub(crate) theme: Option<ThemeDefinition>,
310 pub style_overrides: StyleOverrides,
312 pub chrome_frame: SectionFrameStyle,
314 pub ruled_section_policy: RuledSectionPolicy,
316 pub guide_default_format: GuideDefaultFormat,
318 pub runtime: RenderRuntime,
320}
321
322impl Default for RenderSettings {
323 fn default() -> Self {
324 Self {
325 format: OutputFormat::Auto,
326 format_explicit: false,
327 mode: RenderMode::Auto,
328 color: ColorMode::Auto,
329 unicode: UnicodeMode::Auto,
330 width: None,
331 margin: 0,
332 indent_size: 2,
333 short_list_max: 1,
334 medium_list_max: 5,
335 grid_padding: 4,
336 grid_columns: None,
337 column_weight: 3,
338 table_overflow: TableOverflow::Clip,
339 table_border: TableBorderStyle::Square,
340 help_chrome: HelpChromeSettings::default(),
341 mreg_stack_min_col_width: 10,
342 mreg_stack_overflow_ratio: 200,
343 theme_name: crate::ui::theme::DEFAULT_THEME_NAME.to_string(),
344 theme: None,
345 style_overrides: crate::ui::style::StyleOverrides::default(),
346 chrome_frame: SectionFrameStyle::Top,
347 ruled_section_policy: RuledSectionPolicy::Shared,
348 guide_default_format: GuideDefaultFormat::Guide,
349 runtime: RenderRuntime::default(),
350 }
351 }
352}
353
354#[derive(Debug, Clone, Default)]
383#[must_use]
384pub struct RenderSettingsBuilder {
385 settings: RenderSettings,
386}
387
388impl RenderSettingsBuilder {
389 pub fn new() -> Self {
391 Self::default()
392 }
393
394 pub fn plain(format: OutputFormat) -> Self {
396 Self {
397 settings: RenderSettings {
398 format,
399 format_explicit: false,
400 mode: RenderMode::Plain,
401 color: ColorMode::Never,
402 unicode: UnicodeMode::Never,
403 ..RenderSettings::default()
404 },
405 }
406 }
407
408 pub fn with_format(mut self, format: OutputFormat) -> Self {
410 self.settings.format = format;
411 self
412 }
413
414 pub fn with_format_explicit(mut self, format_explicit: bool) -> Self {
416 self.settings.format_explicit = format_explicit;
417 self
418 }
419
420 pub fn with_mode(mut self, mode: RenderMode) -> Self {
422 self.settings.mode = mode;
423 self
424 }
425
426 pub fn with_color(mut self, color: ColorMode) -> Self {
428 self.settings.color = color;
429 self
430 }
431
432 pub fn with_unicode(mut self, unicode: UnicodeMode) -> Self {
434 self.settings.unicode = unicode;
435 self
436 }
437
438 pub fn with_width(mut self, width: usize) -> Self {
440 self.settings.width = Some(width);
441 self
442 }
443
444 pub fn with_margin(mut self, margin: usize) -> Self {
446 self.settings.margin = margin;
447 self
448 }
449
450 pub fn with_indent_size(mut self, indent_size: usize) -> Self {
452 self.settings.indent_size = indent_size;
453 self
454 }
455
456 pub fn with_table_overflow(mut self, table_overflow: TableOverflow) -> Self {
458 self.settings.table_overflow = table_overflow;
459 self
460 }
461
462 pub fn with_table_border(mut self, table_border: TableBorderStyle) -> Self {
464 self.settings.table_border = table_border;
465 self
466 }
467
468 pub fn with_help_chrome(mut self, help_chrome: HelpChromeSettings) -> Self {
470 self.settings.help_chrome = help_chrome;
471 self
472 }
473
474 pub fn with_theme_name(mut self, theme_name: impl Into<String>) -> Self {
476 self.settings.theme_name = theme_name.into();
477 self
478 }
479
480 pub fn with_style_overrides(mut self, style_overrides: StyleOverrides) -> Self {
482 self.settings.style_overrides = style_overrides;
483 self
484 }
485
486 pub fn with_chrome_frame(mut self, chrome_frame: SectionFrameStyle) -> Self {
488 self.settings.chrome_frame = chrome_frame;
489 self
490 }
491
492 pub fn with_ruled_section_policy(mut self, ruled_section_policy: RuledSectionPolicy) -> Self {
494 self.settings.ruled_section_policy = ruled_section_policy;
495 self
496 }
497
498 pub fn with_guide_default_format(mut self, guide_default_format: GuideDefaultFormat) -> Self {
500 self.settings.guide_default_format = guide_default_format;
501 self
502 }
503
504 pub fn with_runtime(mut self, runtime: RenderRuntime) -> Self {
506 self.settings.runtime = runtime;
507 self
508 }
509
510 pub fn build(self) -> RenderSettings {
512 self.settings
513 }
514}
515
516#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
518pub enum GuideDefaultFormat {
519 #[default]
521 Guide,
522 Inherit,
524}
525
526impl GuideDefaultFormat {
527 pub fn parse(value: &str) -> Option<Self> {
539 match value.trim().to_ascii_lowercase().as_str() {
540 "guide" => Some(Self::Guide),
541 "inherit" | "none" => Some(Self::Inherit),
542 _ => None,
543 }
544 }
545}
546
547#[derive(Debug, Clone, Copy, PartialEq, Eq)]
549pub enum RenderBackend {
550 Plain,
552 Rich,
554}
555
556#[derive(Debug, Clone, Copy, PartialEq, Eq)]
558pub enum TableOverflow {
559 None,
561 Clip,
563 Ellipsis,
565 Wrap,
567}
568
569impl TableOverflow {
570 pub fn parse(value: &str) -> Option<Self> {
582 match value.trim().to_ascii_lowercase().as_str() {
583 "none" | "visible" => Some(Self::None),
584 "clip" | "hidden" | "crop" => Some(Self::Clip),
585 "ellipsis" | "truncate" => Some(Self::Ellipsis),
586 "wrap" | "wrapped" => Some(Self::Wrap),
587 _ => None,
588 }
589 }
590}
591
592#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
594pub enum TableBorderStyle {
595 None,
597 #[default]
599 Square,
600 Round,
602}
603
604impl TableBorderStyle {
605 pub fn parse(value: &str) -> Option<Self> {
617 match value.trim().to_ascii_lowercase().as_str() {
618 "none" | "plain" => Some(Self::None),
619 "square" | "box" | "boxed" => Some(Self::Square),
620 "round" | "rounded" => Some(Self::Round),
621 _ => None,
622 }
623 }
624}
625
626#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
628pub enum HelpTableChrome {
629 Inherit,
631 #[default]
633 None,
634 Square,
636 Round,
638}
639
640impl HelpTableChrome {
641 pub fn parse(value: &str) -> Option<Self> {
653 match value.trim().to_ascii_lowercase().as_str() {
654 "inherit" => Some(Self::Inherit),
655 "none" | "plain" => Some(Self::None),
656 "square" | "box" | "boxed" => Some(Self::Square),
657 "round" | "rounded" => Some(Self::Round),
658 _ => None,
659 }
660 }
661
662 pub fn resolve(self, table_border: TableBorderStyle) -> TableBorderStyle {
679 match self {
680 Self::Inherit => table_border,
681 Self::None => TableBorderStyle::None,
682 Self::Square => TableBorderStyle::Square,
683 Self::Round => TableBorderStyle::Round,
684 }
685 }
686}
687
688impl RenderSettings {
689 pub fn builder() -> RenderSettingsBuilder {
695 RenderSettingsBuilder::new()
696 }
697
698 pub fn test_plain(format: OutputFormat) -> Self {
717 RenderSettingsBuilder::plain(format).build()
718 }
719
720 pub fn prefers_guide_rendering(&self) -> bool {
742 matches!(self.format, OutputFormat::Guide)
743 || (!self.format_explicit
744 && matches!(self.guide_default_format, GuideDefaultFormat::Guide))
745 }
746}
747
748pub fn render_rows(rows: &[Row], settings: &RenderSettings) -> String {
765 render_output(
766 &OutputResult {
767 items: OutputItems::Rows(rows.to_vec()),
768 document: None,
769 meta: Default::default(),
770 },
771 settings,
772 )
773}
774
775pub fn render_output(output: &OutputResult, settings: &RenderSettings) -> String {
792 let plan = settings.resolve_render_plan(output);
793 if matches!(plan.format, OutputFormat::Markdown)
794 && let Some(guide) = GuideView::try_from_output_result(output)
795 {
796 return guide.to_markdown_with_width(plan.render.width);
797 }
798 let document = format::build_document_from_output_plan(output, &plan);
799 renderer::render_document(&document, plan.render)
800}
801
802fn render_guide_document(document: &Document, settings: &RenderSettings) -> String {
803 let mut rendered = render_document_resolved(document, settings.resolve_render_settings());
804 if !rendered.ends_with('\n') {
805 rendered.push('\n');
806 }
807 rendered
808}
809
810pub(crate) fn render_guide_view_with_options(
811 guide: &GuideView,
812 settings: &RenderSettings,
813 options: crate::ui::format::help::GuideRenderOptions<'_>,
814) -> String {
815 if matches!(
816 format::resolve_output_format(&guide.to_output_result(), settings),
817 OutputFormat::Guide
818 ) {
819 let document = crate::ui::format::help::build_guide_document_from_view(guide, options);
820 return render_guide_document(&document, settings);
821 }
822
823 render_output(&guide.to_output_result(), settings)
824}
825
826pub(crate) fn render_guide_payload(
827 config: &crate::config::ResolvedConfig,
828 settings: &RenderSettings,
829 guide: &GuideView,
830) -> String {
831 render_guide_payload_with_layout(
832 guide,
833 settings,
834 crate::ui::presentation::help_layout(config),
835 )
836}
837
838pub(crate) fn render_guide_payload_with_layout(
839 guide: &GuideView,
840 settings: &RenderSettings,
841 layout: crate::ui::presentation::HelpLayout,
842) -> String {
843 let guide_settings = settings.resolve_guide_render_settings();
844 render_guide_view_with_options(
845 guide,
846 settings,
847 crate::ui::format::help::GuideRenderOptions {
848 title_prefix: None,
849 layout,
850 guide: guide_settings,
851 panel_kind: None,
852 },
853 )
854}
855
856pub(crate) fn render_guide_output_with_options(
857 output: &OutputResult,
858 settings: &RenderSettings,
859 options: crate::ui::format::help::GuideRenderOptions<'_>,
860) -> String {
861 if matches!(
862 format::resolve_output_format(output, settings),
863 OutputFormat::Guide
864 ) && let Some(guide) = GuideView::try_from_output_result(output)
865 {
866 return render_guide_view_with_options(&guide, settings, options);
867 }
868
869 render_output(output, settings)
870}
871
872pub(crate) fn guide_render_options<'a>(
873 config: &'a crate::config::ResolvedConfig,
874 settings: &'a RenderSettings,
875) -> crate::ui::format::help::GuideRenderOptions<'a> {
876 let guide_settings = settings.resolve_guide_render_settings();
877 crate::ui::format::help::GuideRenderOptions {
878 title_prefix: None,
879 layout: crate::ui::presentation::help_layout(config),
880 guide: guide_settings,
881 panel_kind: None,
882 }
883}
884
885pub(crate) fn render_structured_output(
886 config: &crate::config::ResolvedConfig,
887 settings: &RenderSettings,
888 output: &OutputResult,
889) -> String {
890 if GuideView::try_from_output_result(output).is_some() {
891 return render_guide_output_with_options(
892 output,
893 settings,
894 guide_render_options(config, settings),
895 );
896 }
897 render_output(output, settings)
898}
899
900pub fn render_document(document: &Document, settings: &RenderSettings) -> String {
921 let resolved = if matches!(settings.format, OutputFormat::Json) {
922 settings.plain_copy_settings().resolve_render_settings()
923 } else {
924 settings.resolve_render_settings()
925 };
926 renderer::render_document(document, resolved)
927}
928
929pub(crate) fn render_document_resolved(
930 document: &Document,
931 settings: ResolvedRenderSettings,
932) -> String {
933 renderer::render_document(document, settings)
934}
935
936pub fn render_rows_for_copy(rows: &[Row], settings: &RenderSettings) -> String {
956 render_output_for_copy(
957 &OutputResult {
958 items: OutputItems::Rows(rows.to_vec()),
959 document: None,
960 meta: Default::default(),
961 },
962 settings,
963 )
964}
965
966pub fn render_output_for_copy(output: &OutputResult, settings: &RenderSettings) -> String {
991 let copy_settings = settings.plain_copy_settings();
992 let plan = copy_settings.resolve_render_plan(output);
993 if matches!(plan.format, OutputFormat::Markdown)
994 && let Some(guide) = GuideView::try_from_output_result(output)
995 {
996 return guide.to_markdown_with_width(plan.render.width);
997 }
998 let document = format::build_document_from_output_plan(output, &plan);
999 renderer::render_document(&document, plan.render)
1000}
1001
1002pub fn render_document_for_copy(document: &Document, settings: &RenderSettings) -> String {
1033 let copy_settings = settings.plain_copy_settings();
1034 let resolved = copy_settings.resolve_render_settings();
1035 renderer::render_document(document, resolved)
1036}
1037
1038pub fn copy_rows_to_clipboard(
1043 rows: &[Row],
1044 settings: &RenderSettings,
1045 clipboard: &clipboard::ClipboardService,
1046) -> Result<(), clipboard::ClipboardError> {
1047 copy_output_to_clipboard(
1048 &OutputResult {
1049 items: OutputItems::Rows(rows.to_vec()),
1050 document: None,
1051 meta: Default::default(),
1052 },
1053 settings,
1054 clipboard,
1055 )
1056}
1057
1058pub fn copy_output_to_clipboard(
1082 output: &OutputResult,
1083 settings: &RenderSettings,
1084 clipboard: &clipboard::ClipboardService,
1085) -> Result<(), clipboard::ClipboardError> {
1086 let text = render_output_for_copy(output, settings);
1087 clipboard.copy_text(&text)
1088}
1089
1090#[cfg(test)]
1091mod tests {
1092 use super::{
1093 GuideDefaultFormat, HelpChromeSettings, HelpTableChrome, RenderBackend, RenderRuntime,
1094 RenderSettings, RenderSettingsBuilder, TableBorderStyle, TableOverflow, format,
1095 render_document, render_document_for_copy, render_output, render_output_for_copy,
1096 render_rows, render_rows_for_copy,
1097 };
1098 use crate::core::output::{ColorMode, OutputFormat, RenderMode, UnicodeMode};
1099 use crate::core::output_model::OutputResult;
1100 use crate::core::row::Row;
1101 use crate::guide::GuideView;
1102 use crate::ui::document::{Block, Document, JsonBlock, MregValue, TableStyle};
1103 use serde_json::json;
1104
1105 fn settings(format: OutputFormat) -> RenderSettings {
1106 RenderSettings {
1107 mode: RenderMode::Auto,
1108 ..RenderSettings::test_plain(format)
1109 }
1110 }
1111
1112 #[test]
1113 fn document_builder_selects_auto_and_explicit_block_shapes_unit() {
1114 let value_rows = vec![{
1115 let mut row = Row::new();
1116 row.insert("value".to_string(), json!("hello"));
1117 row
1118 }];
1119 let document = format::build_document(&value_rows, &settings(OutputFormat::Auto));
1120 assert!(matches!(document.blocks[0], Block::Value(_)));
1121
1122 let mreg_rows = vec![{
1123 let mut row = Row::new();
1124 row.insert("uid".to_string(), json!("oistes"));
1125 row
1126 }];
1127 let document = format::build_document(&mreg_rows, &settings(OutputFormat::Auto));
1128 assert!(matches!(document.blocks[0], Block::Mreg(_)));
1129
1130 let table_rows = vec![
1131 {
1132 let mut row = Row::new();
1133 row.insert("uid".to_string(), json!("one"));
1134 row
1135 },
1136 {
1137 let mut row = Row::new();
1138 row.insert("uid".to_string(), json!("two"));
1139 row
1140 },
1141 ];
1142 let document = format::build_document(&table_rows, &settings(OutputFormat::Auto));
1143 assert!(matches!(document.blocks[0], Block::Table(_)));
1144
1145 let rich_rows = vec![{
1146 let mut row = Row::new();
1147 row.insert("uid".to_string(), json!("oistes"));
1148 row.insert("groups".to_string(), json!(["a", "b"]));
1149 row
1150 }];
1151 let document = format::build_document(&rich_rows, &settings(OutputFormat::Mreg));
1152 let Block::Mreg(block) = &document.blocks[0] else {
1153 panic!("expected mreg block");
1154 };
1155 assert_eq!(block.rows.len(), 1);
1156 assert!(
1157 block.rows[0]
1158 .entries
1159 .iter()
1160 .any(|entry| matches!(entry.value, MregValue::Scalar(_)))
1161 );
1162 assert!(
1163 block.rows[0]
1164 .entries
1165 .iter()
1166 .any(|entry| matches!(entry.value, MregValue::VerticalList(_)))
1167 );
1168
1169 let markdown_rows = vec![{
1170 let mut row = Row::new();
1171 row.insert("uid".to_string(), json!("oistes"));
1172 row
1173 }];
1174 let document = format::build_document(&markdown_rows, &settings(OutputFormat::Markdown));
1175 let Block::Table(table) = &document.blocks[0] else {
1176 panic!("expected table block");
1177 };
1178 assert_eq!(table.style, TableStyle::Markdown);
1179 }
1180
1181 #[test]
1182 fn semantic_guide_markdown_output_and_copy_remain_section_based_unit() {
1183 let output =
1184 GuideView::from_text("Usage: osp history <COMMAND>\n\nCommands:\n list Show\n")
1185 .to_output_result();
1186 let settings = RenderSettings {
1187 format: OutputFormat::Markdown,
1188 format_explicit: true,
1189 ..settings(OutputFormat::Markdown)
1190 };
1191
1192 let rendered = render_output(&output, &settings);
1193 let copied = render_output_for_copy(&output, &settings);
1194
1195 for text in [&rendered, &copied] {
1196 assert!(text.contains("## Usage"));
1197 assert!(text.contains("## Commands"));
1198 assert!(text.contains("- `list` Show"));
1199 assert!(!text.contains("| name"));
1200 }
1201 assert!(!copied.contains("\x1b["));
1202 }
1203
1204 #[test]
1205 fn render_builders_and_parse_helpers_cover_configuration_surface_unit() {
1206 let runtime = RenderRuntime::builder()
1207 .with_stdout_is_tty(true)
1208 .with_terminal("xterm-256color")
1209 .with_no_color(true)
1210 .with_width(98)
1211 .with_locale_utf8(false)
1212 .build();
1213 assert_eq!(
1214 runtime,
1215 RenderRuntime {
1216 stdout_is_tty: true,
1217 terminal: Some("xterm-256color".to_string()),
1218 no_color: true,
1219 width: Some(98),
1220 locale_utf8: Some(false),
1221 }
1222 );
1223
1224 let settings = RenderSettings::builder()
1225 .with_format(OutputFormat::Markdown)
1226 .with_format_explicit(true)
1227 .with_mode(RenderMode::Rich)
1228 .with_color(ColorMode::Always)
1229 .with_unicode(UnicodeMode::Auto)
1230 .with_width(98)
1231 .with_margin(2)
1232 .with_indent_size(4)
1233 .with_table_overflow(TableOverflow::Wrap)
1234 .with_table_border(TableBorderStyle::Round)
1235 .with_help_chrome(HelpChromeSettings {
1236 table_chrome: HelpTableChrome::Inherit,
1237 ..HelpChromeSettings::default()
1238 })
1239 .with_theme_name("dracula")
1240 .with_style_overrides(Default::default())
1241 .with_chrome_frame(crate::ui::SectionFrameStyle::Round)
1242 .with_guide_default_format(GuideDefaultFormat::Inherit)
1243 .with_runtime(runtime.clone())
1244 .build();
1245 assert_eq!(settings.format, OutputFormat::Markdown);
1246 assert!(settings.format_explicit);
1247 assert_eq!(settings.mode, RenderMode::Rich);
1248 assert_eq!(settings.color, ColorMode::Always);
1249 assert_eq!(settings.unicode, UnicodeMode::Auto);
1250 assert_eq!(settings.width, Some(98));
1251 assert_eq!(settings.margin, 2);
1252 assert_eq!(settings.indent_size, 4);
1253 assert_eq!(settings.table_overflow, TableOverflow::Wrap);
1254 assert_eq!(settings.table_border, TableBorderStyle::Round);
1255 assert_eq!(settings.help_chrome.table_chrome, HelpTableChrome::Inherit);
1256 assert_eq!(settings.theme_name, "dracula");
1257 assert_eq!(settings.chrome_frame, crate::ui::SectionFrameStyle::Round);
1258 assert_eq!(settings.guide_default_format, GuideDefaultFormat::Inherit);
1259 assert_eq!(settings.runtime, runtime);
1260
1261 let plain = RenderSettingsBuilder::plain(OutputFormat::Json).build();
1262 assert_eq!(plain.mode, RenderMode::Plain);
1263 assert_eq!(plain.color, ColorMode::Never);
1264 assert_eq!(plain.unicode, UnicodeMode::Never);
1265
1266 assert_eq!(
1267 GuideDefaultFormat::parse("none"),
1268 Some(GuideDefaultFormat::Inherit)
1269 );
1270 assert_eq!(GuideDefaultFormat::parse("wat"), None);
1271 assert_eq!(
1272 HelpTableChrome::parse("round"),
1273 Some(HelpTableChrome::Round)
1274 );
1275 assert_eq!(HelpTableChrome::parse("wat"), None);
1276 assert_eq!(
1277 HelpTableChrome::Inherit.resolve(TableBorderStyle::Round),
1278 TableBorderStyle::Round
1279 );
1280 assert_eq!(
1281 HelpTableChrome::None.resolve(TableBorderStyle::Square),
1282 TableBorderStyle::None
1283 );
1284 assert_eq!(
1285 HelpTableChrome::Square.resolve(TableBorderStyle::None),
1286 TableBorderStyle::Square
1287 );
1288 assert_eq!(
1289 TableBorderStyle::parse("none"),
1290 Some(TableBorderStyle::None)
1291 );
1292 assert_eq!(
1293 TableBorderStyle::parse("box"),
1294 Some(TableBorderStyle::Square)
1295 );
1296 assert_eq!(
1297 TableBorderStyle::parse("square"),
1298 Some(TableBorderStyle::Square)
1299 );
1300 assert_eq!(
1301 TableBorderStyle::parse("round"),
1302 Some(TableBorderStyle::Round)
1303 );
1304 assert_eq!(
1305 TableBorderStyle::parse("rounded"),
1306 Some(TableBorderStyle::Round)
1307 );
1308 assert_eq!(TableBorderStyle::parse("mystery"), None);
1309 assert_eq!(TableOverflow::parse("visible"), Some(TableOverflow::None));
1310 assert_eq!(TableOverflow::parse("crop"), Some(TableOverflow::Clip));
1311 assert_eq!(
1312 TableOverflow::parse("truncate"),
1313 Some(TableOverflow::Ellipsis)
1314 );
1315 assert_eq!(TableOverflow::parse("wrapped"), Some(TableOverflow::Wrap));
1316 assert_eq!(TableOverflow::parse("other"), None);
1317 }
1318
1319 #[test]
1320 fn render_resolution_covers_public_helpers_mode_runtime_and_force_rules_unit() {
1321 let rows = vec![{
1322 let mut row = Row::new();
1323 row.insert("uid".to_string(), json!("alice"));
1324 row
1325 }];
1326 let rendered = render_rows(&rows, &settings(OutputFormat::Table));
1327 assert!(rendered.contains("uid"));
1328 assert!(rendered.contains("alice"));
1329
1330 let dumb_terminal = RenderSettings {
1331 mode: RenderMode::Rich,
1332 color: ColorMode::Auto,
1333 unicode: UnicodeMode::Auto,
1334 width: Some(0),
1335 grid_columns: Some(0),
1336 runtime: RenderRuntime {
1337 stdout_is_tty: true,
1338 terminal: Some("dumb".to_string()),
1339 no_color: false,
1340 width: Some(0),
1341 locale_utf8: Some(true),
1342 },
1343 ..RenderSettings::test_plain(OutputFormat::Table)
1344 };
1345 let dumb_resolved = dumb_terminal.resolve_render_settings();
1346 assert_eq!(dumb_resolved.backend, RenderBackend::Rich);
1347 assert!(dumb_resolved.color);
1348 assert!(!dumb_resolved.unicode);
1349 assert_eq!(dumb_resolved.width, None);
1350 assert_eq!(dumb_resolved.grid_columns, None);
1351
1352 let locale_false = RenderSettings {
1353 mode: RenderMode::Rich,
1354 color: ColorMode::Auto,
1355 unicode: UnicodeMode::Auto,
1356 runtime: RenderRuntime {
1357 stdout_is_tty: true,
1358 terminal: Some("xterm-256color".to_string()),
1359 no_color: false,
1360 width: Some(72),
1361 locale_utf8: Some(false),
1362 },
1363 ..RenderSettings::test_plain(OutputFormat::Table)
1364 };
1365 let locale_resolved = locale_false.resolve_render_settings();
1366 assert!(locale_resolved.color);
1367 assert!(!locale_resolved.unicode);
1368 assert_eq!(locale_resolved.width, Some(72));
1369
1370 let plain = RenderSettings {
1371 format: OutputFormat::Table,
1372 color: ColorMode::Always,
1373 unicode: UnicodeMode::Always,
1374 ..RenderSettings::test_plain(OutputFormat::Table)
1375 };
1376 let resolved = plain.resolve_render_settings();
1377 assert_eq!(resolved.backend, RenderBackend::Plain);
1378 assert!(!resolved.color);
1379 assert!(!resolved.unicode);
1380
1381 let rich = RenderSettings {
1382 format: OutputFormat::Table,
1383 mode: RenderMode::Rich,
1384 color: ColorMode::Always,
1385 unicode: UnicodeMode::Always,
1386 ..RenderSettings::test_plain(OutputFormat::Table)
1387 };
1388 let resolved = rich.resolve_render_settings();
1389 assert_eq!(resolved.backend, RenderBackend::Rich);
1390 assert!(resolved.color);
1391 assert!(resolved.unicode);
1392 let auto = RenderSettings {
1393 mode: RenderMode::Auto,
1394 color: ColorMode::Auto,
1395 unicode: UnicodeMode::Auto,
1396 runtime: super::RenderRuntime {
1397 stdout_is_tty: true,
1398 terminal: Some("dumb".to_string()),
1399 no_color: false,
1400 width: Some(72),
1401 locale_utf8: Some(false),
1402 },
1403 ..RenderSettings::test_plain(OutputFormat::Table)
1404 };
1405 let resolved = auto.resolve_render_settings();
1406 assert_eq!(resolved.backend, RenderBackend::Plain);
1407 assert!(!resolved.color);
1408 assert!(!resolved.unicode);
1409 assert_eq!(resolved.width, Some(72));
1410
1411 let forced_color = RenderSettings {
1412 mode: RenderMode::Auto,
1413 color: ColorMode::Always,
1414 unicode: UnicodeMode::Auto,
1415 runtime: super::RenderRuntime {
1416 stdout_is_tty: false,
1417 terminal: Some("xterm-256color".to_string()),
1418 no_color: false,
1419 width: Some(80),
1420 locale_utf8: Some(true),
1421 },
1422 ..RenderSettings::test_plain(OutputFormat::Table)
1423 };
1424 let resolved = forced_color.resolve_render_settings();
1425 assert_eq!(resolved.backend, RenderBackend::Rich);
1426 assert!(resolved.color);
1427
1428 let forced_unicode = RenderSettings {
1429 mode: RenderMode::Auto,
1430 color: ColorMode::Auto,
1431 unicode: UnicodeMode::Always,
1432 runtime: super::RenderRuntime {
1433 stdout_is_tty: false,
1434 terminal: Some("dumb".to_string()),
1435 no_color: true,
1436 width: Some(64),
1437 locale_utf8: Some(false),
1438 },
1439 ..RenderSettings::test_plain(OutputFormat::Table)
1440 };
1441 let resolved = forced_unicode.resolve_render_settings();
1442 assert_eq!(resolved.backend, RenderBackend::Rich);
1443 assert!(!resolved.color);
1444 assert!(resolved.unicode);
1445
1446 let guide_settings = RenderSettings {
1447 help_chrome: HelpChromeSettings {
1448 table_chrome: HelpTableChrome::Inherit,
1449 entry_indent: Some(4),
1450 entry_gap: Some(3),
1451 section_spacing: Some(0),
1452 },
1453 table_border: TableBorderStyle::Round,
1454 chrome_frame: crate::ui::SectionFrameStyle::TopBottom,
1455 ..RenderSettings::test_plain(OutputFormat::Guide)
1456 };
1457 let guide_resolved = guide_settings.resolve_guide_render_settings();
1458 assert_eq!(
1459 guide_resolved.frame_style,
1460 crate::ui::SectionFrameStyle::TopBottom
1461 );
1462 assert_eq!(
1463 guide_resolved.help_chrome.table_border,
1464 TableBorderStyle::Round
1465 );
1466 assert_eq!(guide_resolved.help_chrome.entry_indent, Some(4));
1467 assert_eq!(guide_resolved.help_chrome.entry_gap, Some(3));
1468 assert_eq!(guide_resolved.help_chrome.section_spacing, Some(0));
1469
1470 let mreg_settings = RenderSettings {
1471 short_list_max: 0,
1472 medium_list_max: 0,
1473 indent_size: 0,
1474 mreg_stack_min_col_width: 0,
1475 mreg_stack_overflow_ratio: 10,
1476 ..RenderSettings::test_plain(OutputFormat::Mreg)
1477 };
1478 let mreg_resolved = mreg_settings.resolve_mreg_build_settings();
1479 assert_eq!(mreg_resolved.short_list_max, 1);
1480 assert_eq!(mreg_resolved.medium_list_max, 2);
1481 assert_eq!(mreg_resolved.indent_size, 1);
1482 assert_eq!(mreg_resolved.stack_min_col_width, 1);
1483 assert_eq!(mreg_resolved.stack_overflow_ratio, 100);
1484 }
1485
1486 #[test]
1487 fn copy_helpers_force_plain_copy_mode_for_rows_documents_and_json_unit() {
1488 let table_rows = vec![{
1489 let mut row = Row::new();
1490 row.insert("uid".to_string(), json!("oistes"));
1491 row.insert(
1492 "description".to_string(),
1493 json!("very long text that will be shown"),
1494 );
1495 row
1496 }];
1497 let rich_table = RenderSettings {
1498 format: OutputFormat::Table,
1499 mode: RenderMode::Rich,
1500 color: ColorMode::Always,
1501 unicode: UnicodeMode::Always,
1502 ..RenderSettings::test_plain(OutputFormat::Table)
1503 };
1504 let table_copy = render_rows_for_copy(&table_rows, &rich_table);
1505 assert!(!table_copy.contains("\x1b["));
1506 assert!(!table_copy.contains('┌'));
1507 assert!(table_copy.contains('+'));
1508
1509 let value_rows = vec![{
1510 let mut row = Row::new();
1511 row.insert("value".to_string(), json!("hello"));
1512 row
1513 }];
1514 let rich_value = RenderSettings {
1515 mode: RenderMode::Rich,
1516 color: ColorMode::Always,
1517 unicode: UnicodeMode::Always,
1518 ..RenderSettings::test_plain(OutputFormat::Value)
1519 };
1520 let value_copy = render_rows_for_copy(&value_rows, &rich_value);
1521 assert_eq!(value_copy.trim(), "hello");
1522 assert!(!value_copy.contains("\x1b["));
1523
1524 let document = crate::ui::Document {
1525 blocks: vec![Block::Line(crate::ui::LineBlock {
1526 parts: vec![crate::ui::LinePart {
1527 text: "hello".to_string(),
1528 token: None,
1529 }],
1530 })],
1531 };
1532 let rich_document = RenderSettings {
1533 mode: RenderMode::Rich,
1534 color: ColorMode::Always,
1535 unicode: UnicodeMode::Always,
1536 ..RenderSettings::test_plain(OutputFormat::Table)
1537 };
1538 let rich = render_document(&document, &rich_document);
1539 let copied = render_document_for_copy(&document, &rich_document);
1540
1541 assert!(rich.contains("hello"));
1542 assert!(copied.contains("hello"));
1543 assert!(!copied.contains("\x1b["));
1544
1545 let json_rows = vec![{
1546 let mut row = Row::new();
1547 row.insert("uid".to_string(), json!("alice"));
1548 row.insert("count".to_string(), json!(2));
1549 row
1550 }];
1551 let json_settings = RenderSettings {
1552 format: OutputFormat::Json,
1553 mode: RenderMode::Rich,
1554 color: ColorMode::Always,
1555 unicode: UnicodeMode::Always,
1556 ..RenderSettings::test_plain(OutputFormat::Json)
1557 };
1558
1559 let output = OutputResult::from_rows(json_rows);
1560 let rendered = render_output(&output, &json_settings);
1561 let copied = render_output_for_copy(&output, &json_settings);
1562 let json_document = Document {
1563 blocks: vec![Block::Json(JsonBlock {
1564 payload: json!([{ "uid": "alice", "count": 2 }]),
1565 })],
1566 };
1567 let rendered_document = render_document(&json_document, &json_settings);
1568
1569 assert!(rendered.contains("\"uid\""));
1570 assert_eq!(
1571 rendered,
1572 "[\n {\n \"uid\": \"alice\",\n \"count\": 2\n }\n]\n"
1573 );
1574 assert!(!rendered.contains("\x1b["));
1575 assert_eq!(
1576 rendered_document,
1577 "[\n {\n \"uid\": \"alice\",\n \"count\": 2\n }\n]\n"
1578 );
1579 assert!(!rendered_document.contains("\x1b["));
1580 assert_eq!(
1581 copied,
1582 "[\n {\n \"uid\": \"alice\",\n \"count\": 2\n }\n]\n"
1583 );
1584 assert!(!copied.contains("\x1b["));
1585 }
1586}