Skip to main content

osp_cli/ui/
mod.rs

1//! The UI module turns structured output into predictable terminal text while
2//! keeping rendering decisions separate from business logic.
3//!
4//! Data changes shape four times on the way from DSL output to terminal text:
5//!
6//! ```text
7//! OutputResult { items: Rows([{uid:"alice", cn:"Alice"},...]), meta }
8//!       │
9//!       ▼  RenderSettings::resolve_render_plan(output)
10//!       │  selects format: explicit > metadata hint > shape-inference
11//!       │    Groups        → Table
12//!       │    single-value rows → Value
13//!       │    1 row         → Mreg (key/value pairs)
14//!       │    many rows     → Table
15//!       │
16//! ResolvedRenderPlan {
17//!     format: Table,
18//!     render: ResolvedRenderSettings { backend: Rich|Plain, color, width, theme, … },
19//!     guide:  ResolvedGuideRenderSettings,
20//!     mreg:   ResolvedMregRenderSettings,
21//! }
22//!       │
23//!       ▼  format::build_document_from_output_plan(output, plan)
24//!       │  dispatches by format into typed block variants
25//!       │    Table/Markdown → Block::Table(TableBlock { headers, rows: Vec<Vec<Value>>, align })
26//!       │    Json           → Block::Json(JsonBlock { value })
27//!       │    Mreg           → Block::Mreg(MregBlock { rows: Vec<MregRow> })
28//!       │    Value          → Block::Value(ValueBlock { values: Vec<String> })
29//!       │    Guide          → Block::Panel + Block::Table + Block::Line …
30//!       │
31//! Document { blocks: Vec<Block> }
32//!       │
33//!       ▼  renderer::render_document(document, resolved_settings)
34//!       │  precomputes LayoutContext once (width, unicode, margin)
35//!       │  renders each block: Rich = color + box-drawing; Plain = ASCII only
36//!       │
37//! String  ←  terminal text ready for stdout
38//! ```
39//!
40//! The three public entry points cover the common call sites:
41//!
42//! - [`crate::ui::render_output`] — full pipeline from `OutputResult` to `String`
43//! - [`crate::ui::render_rows`] — convenience wrapper when you already have `Vec<Row>`
44//! - [`crate::ui::render_document`] — skip straight to rendering a pre-built `Document`
45//!
46//! Minimal direct-render path:
47//!
48//! ```
49//! use osp_cli::core::output::OutputFormat;
50//! use osp_cli::row;
51//! use osp_cli::ui::{RenderSettings, render_rows};
52//!
53//! let rendered = render_rows(
54//!     &[row! { "uid" => "alice" }],
55//!     &RenderSettings::test_plain(OutputFormat::Json),
56//! );
57//!
58//! assert!(rendered.contains("\"uid\": \"alice\""));
59//! ```
60//!
61//! Each has a `_for_copy` sibling that forces Plain backend and strips
62//! box-drawing/ANSI for clipboard-safe output.
63//!
64//! Debugging tip: keep "document shaping" and "terminal rendering" separate.
65//! If output looks structurally wrong (missing columns, wrong format), the
66//! bug is in [`format`]. If it looks semantically right but visually garbled
67//! (bad color, wrong width), the bug is in the renderer or settings resolution.
68//!
69//! Contract:
70//!
71//! - UI code may depend on structured output and render settings
72//! - it should not own config precedence, command execution, or provider I/O
73//! - terminal styling decisions should stay here rather than leaking into the
74//!   rest of the app
75//!
76//! Public API shape:
77//!
78//! - semantic payloads like [`crate::ui::Document`] stay direct and cheap to
79//!   inspect
80//! - [`crate::ui::RenderRuntimeBuilder`] and
81//!   [`crate::ui::RenderSettingsBuilder`] are the guided construction path for
82//!   the heavier rendering configuration surfaces
83//! - [`crate::ui::ResolvedRenderSettings`] stays a derived value, not another
84//!   mutable configuration object
85//! - guided render configuration follows the crate-wide naming rule:
86//!   `builder(...)` returns `*Builder`, builder setters use `with_*`, and
87//!   `build()` is the terminal step
88//! - callers that only need a stable default baseline can use
89//!   [`crate::ui::RenderSettings::builder`],
90//!   [`crate::ui::RenderRuntime::builder`], or
91//!   [`crate::ui::RenderSettings::test_plain`]
92
93pub mod chrome;
94/// Clipboard integration helpers for copy-safe output flows.
95pub mod clipboard;
96mod display;
97pub mod document;
98pub(crate) mod document_model;
99pub(crate) mod format;
100/// Lightweight inline-markup parsing and rendering helpers.
101pub mod inline;
102pub mod interactive;
103mod layout;
104pub mod messages;
105pub(crate) mod presentation;
106mod renderer;
107mod resolution;
108/// Semantic style tokens and explicit style overrides layered over the theme.
109pub mod style;
110/// Built-in theme definitions and theme lookup helpers.
111pub 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/// Runtime terminal characteristics used when resolving render behavior.
147#[derive(Debug, Clone, Default, PartialEq, Eq)]
148#[non_exhaustive]
149#[must_use]
150pub struct RenderRuntime {
151    /// Whether standard output is attached to a TTY.
152    pub stdout_is_tty: bool,
153    /// Terminal program identifier when known.
154    pub terminal: Option<String>,
155    /// Whether color should be suppressed regardless of theme.
156    pub no_color: bool,
157    /// Measured terminal width, when available.
158    pub width: Option<usize>,
159    /// Whether the locale is known to support UTF-8.
160    pub locale_utf8: Option<bool>,
161}
162
163impl RenderRuntime {
164    /// Starts building runtime terminal facts for render resolution.
165    ///
166    /// Prefer this when a host or test already knows terminal facts and wants
167    /// auto-resolved rendering to use those values consistently.
168    ///
169    /// # Examples
170    ///
171    /// ```
172    /// use osp_cli::ui::RenderRuntime;
173    ///
174    /// let runtime = RenderRuntime::builder()
175    ///     .with_stdout_is_tty(true)
176    ///     .with_terminal("xterm-256color")
177    ///     .with_width(120)
178    ///     .with_locale_utf8(true)
179    ///     .build();
180    ///
181    /// assert_eq!(runtime.terminal.as_deref(), Some("xterm-256color"));
182    /// assert_eq!(runtime.width, Some(120));
183    /// ```
184    pub fn builder() -> RenderRuntimeBuilder {
185        RenderRuntimeBuilder::new()
186    }
187}
188
189/// Builder for [`RenderRuntime`].
190///
191/// Use this when render policy should remain on `Auto`, but the caller wants to
192/// control the terminal facts that auto-resolution sees. In tests, this is the
193/// easiest way to force deterministic width or TTY behavior without mutating
194/// process state.
195#[derive(Debug, Clone, Default)]
196#[must_use]
197pub struct RenderRuntimeBuilder {
198    runtime: RenderRuntime,
199}
200
201impl RenderRuntimeBuilder {
202    /// Creates a builder seeded with [`RenderRuntime::default`].
203    pub fn new() -> Self {
204        Self::default()
205    }
206
207    /// Sets whether standard output is attached to a TTY.
208    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    /// Sets the terminal program identifier.
214    pub fn with_terminal(mut self, terminal: impl Into<String>) -> Self {
215        self.runtime.terminal = Some(terminal.into());
216        self
217    }
218
219    /// Sets whether color should be suppressed regardless of theme.
220    pub fn with_no_color(mut self, no_color: bool) -> Self {
221        self.runtime.no_color = no_color;
222        self
223    }
224
225    /// Sets the measured terminal width.
226    pub fn with_width(mut self, width: usize) -> Self {
227        self.runtime.width = Some(width);
228        self
229    }
230
231    /// Sets whether the locale is known to support UTF-8.
232    pub fn with_locale_utf8(mut self, locale_utf8: bool) -> Self {
233        self.runtime.locale_utf8 = Some(locale_utf8);
234        self
235    }
236
237    /// Builds the runtime terminal facts.
238    pub fn build(self) -> RenderRuntime {
239        self.runtime
240    }
241}
242
243/// User-configurable settings for rendering CLI output.
244#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
245pub struct HelpChromeSettings {
246    /// Border style override for help/guide tables.
247    pub table_chrome: HelpTableChrome,
248    /// Explicit indentation override for help entries.
249    pub entry_indent: Option<usize>,
250    /// Explicit gap override between help entry columns.
251    pub entry_gap: Option<usize>,
252    /// Explicit spacing override between help sections.
253    pub section_spacing: Option<usize>,
254}
255
256/// User-configurable settings for rendering CLI output.
257///
258/// This is the durable caller-facing render policy. `format`, `mode`, `color`,
259/// and `unicode` express caller intent; `runtime` supplies the terminal facts
260/// used when any of those stay on `Auto`. Most embedders should start with
261/// [`RenderSettings::builder`], while tests and deterministic snapshots should
262/// usually start with [`RenderSettings::test_plain`].
263#[derive(Debug, Clone)]
264#[non_exhaustive]
265#[must_use]
266pub struct RenderSettings {
267    /// Preferred output format.
268    pub format: OutputFormat,
269    /// Whether `format` was chosen explicitly by the caller.
270    pub format_explicit: bool,
271    /// Preferred rendering mode.
272    pub mode: RenderMode,
273    /// Color behavior selection.
274    pub color: ColorMode,
275    /// Unicode behavior selection.
276    pub unicode: UnicodeMode,
277    /// Explicit width override for rendering.
278    pub width: Option<usize>,
279    /// Left margin applied to rendered blocks.
280    pub margin: usize,
281    /// Indentation width used for nested structures.
282    pub indent_size: usize,
283    /// Maximum list length rendered in compact form.
284    pub short_list_max: usize,
285    /// Maximum list length rendered in medium form before expanding further.
286    pub medium_list_max: usize,
287    /// Horizontal padding between grid columns.
288    pub grid_padding: usize,
289    /// Explicit grid column count override.
290    pub grid_columns: Option<usize>,
291    /// Relative weighting for adaptive grid columns.
292    pub column_weight: usize,
293    /// Overflow policy for table cells.
294    pub table_overflow: TableOverflow,
295    /// Border style for general table rendering.
296    pub table_border: TableBorderStyle,
297    /// Help/guide-specific chrome settings.
298    pub help_chrome: HelpChromeSettings,
299    /// Minimum width before stacked MREG columns are used.
300    pub mreg_stack_min_col_width: usize,
301    /// Threshold controlling when MREG content overflows into stacked mode.
302    pub mreg_stack_overflow_ratio: usize,
303    /// Selected theme name.
304    pub theme_name: String,
305    /// Cached resolved theme derived from `theme_name`.
306    ///
307    /// This stays crate-internal so external callers cannot create
308    /// contradictory `theme_name` / resolved-theme pairs.
309    pub(crate) theme: Option<ThemeDefinition>,
310    /// Per-token style overrides layered on top of the theme.
311    pub style_overrides: StyleOverrides,
312    /// Section frame style used for grouped chrome.
313    pub chrome_frame: SectionFrameStyle,
314    /// Placement policy for ruled section separators across sibling sections.
315    pub ruled_section_policy: RuledSectionPolicy,
316    /// Fallback behavior for semantic guide output.
317    pub guide_default_format: GuideDefaultFormat,
318    /// Runtime terminal facts used during auto-resolution.
319    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/// Builder for [`RenderSettings`].
355///
356/// Use [`RenderSettingsBuilder::new`] for normal caller-facing settings that may
357/// still auto-resolve from terminal facts. Use [`RenderSettingsBuilder::plain`]
358/// for deterministic tests, docs, and copy-oriented flows where ANSI and rich
359/// terminal behavior should already be disabled.
360///
361/// # Examples
362///
363/// ```
364/// use osp_cli::core::output::OutputFormat;
365/// use osp_cli::ui::{GuideDefaultFormat, RenderRuntime, RenderSettings};
366///
367/// let settings = RenderSettings::builder()
368///     .with_format(OutputFormat::Auto)
369///     .with_guide_default_format(GuideDefaultFormat::Guide)
370///     .with_runtime(
371///         RenderRuntime::builder()
372///             .with_stdout_is_tty(true)
373///             .with_terminal("xterm-256color")
374///             .with_width(100)
375///             .build(),
376///     )
377///     .build();
378///
379/// assert_eq!(settings.runtime.width, Some(100));
380/// assert!(settings.prefers_guide_rendering());
381/// ```
382#[derive(Debug, Clone, Default)]
383#[must_use]
384pub struct RenderSettingsBuilder {
385    settings: RenderSettings,
386}
387
388impl RenderSettingsBuilder {
389    /// Creates a builder seeded with [`RenderSettings::default`].
390    pub fn new() -> Self {
391        Self::default()
392    }
393
394    /// Creates a builder seeded with [`RenderSettings::test_plain`].
395    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    /// Sets the preferred output format.
409    pub fn with_format(mut self, format: OutputFormat) -> Self {
410        self.settings.format = format;
411        self
412    }
413
414    /// Sets whether the output format was chosen explicitly.
415    pub fn with_format_explicit(mut self, format_explicit: bool) -> Self {
416        self.settings.format_explicit = format_explicit;
417        self
418    }
419
420    /// Sets the preferred rendering mode.
421    pub fn with_mode(mut self, mode: RenderMode) -> Self {
422        self.settings.mode = mode;
423        self
424    }
425
426    /// Sets color behavior.
427    pub fn with_color(mut self, color: ColorMode) -> Self {
428        self.settings.color = color;
429        self
430    }
431
432    /// Sets Unicode behavior.
433    pub fn with_unicode(mut self, unicode: UnicodeMode) -> Self {
434        self.settings.unicode = unicode;
435        self
436    }
437
438    /// Sets an explicit width override.
439    pub fn with_width(mut self, width: usize) -> Self {
440        self.settings.width = Some(width);
441        self
442    }
443
444    /// Sets the left margin.
445    pub fn with_margin(mut self, margin: usize) -> Self {
446        self.settings.margin = margin;
447        self
448    }
449
450    /// Sets the indentation width for nested structures.
451    pub fn with_indent_size(mut self, indent_size: usize) -> Self {
452        self.settings.indent_size = indent_size;
453        self
454    }
455
456    /// Sets the overflow policy for table cells.
457    pub fn with_table_overflow(mut self, table_overflow: TableOverflow) -> Self {
458        self.settings.table_overflow = table_overflow;
459        self
460    }
461
462    /// Sets the general table border style.
463    pub fn with_table_border(mut self, table_border: TableBorderStyle) -> Self {
464        self.settings.table_border = table_border;
465        self
466    }
467
468    /// Sets the grouped help/guide chrome settings.
469    pub fn with_help_chrome(mut self, help_chrome: HelpChromeSettings) -> Self {
470        self.settings.help_chrome = help_chrome;
471        self
472    }
473
474    /// Sets the selected theme name.
475    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    /// Sets style overrides layered over the theme.
481    pub fn with_style_overrides(mut self, style_overrides: StyleOverrides) -> Self {
482        self.settings.style_overrides = style_overrides;
483        self
484    }
485
486    /// Sets the section frame style.
487    pub fn with_chrome_frame(mut self, chrome_frame: SectionFrameStyle) -> Self {
488        self.settings.chrome_frame = chrome_frame;
489        self
490    }
491
492    /// Sets how ruled separators are shared across sibling sections.
493    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    /// Sets the default guide-rendering preference.
499    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    /// Sets the runtime terminal facts used during auto-resolution.
505    pub fn with_runtime(mut self, runtime: RenderRuntime) -> Self {
506        self.settings.runtime = runtime;
507        self
508    }
509
510    /// Builds the render settings value.
511    pub fn build(self) -> RenderSettings {
512        self.settings
513    }
514}
515
516/// Default output format to use when guide rendering is not explicitly requested.
517#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
518pub enum GuideDefaultFormat {
519    /// Prefer semantic guide output when the caller did not request a format.
520    #[default]
521    Guide,
522    /// Inherit the caller-selected format without forcing guide mode.
523    Inherit,
524}
525
526impl GuideDefaultFormat {
527    /// Parses the guide fallback mode used when no explicit output format wins.
528    ///
529    /// # Examples
530    ///
531    /// ```
532    /// use osp_cli::ui::GuideDefaultFormat;
533    ///
534    /// assert_eq!(GuideDefaultFormat::parse("guide"), Some(GuideDefaultFormat::Guide));
535    /// assert_eq!(GuideDefaultFormat::parse("none"), Some(GuideDefaultFormat::Inherit));
536    /// assert_eq!(GuideDefaultFormat::parse("wat"), None);
537    /// ```
538    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/// Rendering backend selected for the current output pass.
548#[derive(Debug, Clone, Copy, PartialEq, Eq)]
549pub enum RenderBackend {
550    /// Render without terminal-rich features.
551    Plain,
552    /// Render using ANSI and richer terminal affordances.
553    Rich,
554}
555
556/// Overflow strategy for table cell content.
557#[derive(Debug, Clone, Copy, PartialEq, Eq)]
558pub enum TableOverflow {
559    /// Leave overflow management to the terminal.
560    None,
561    /// Hard-clip overflowing cell content.
562    Clip,
563    /// Truncate overflowing content with an ellipsis marker.
564    Ellipsis,
565    /// Wrap overflowing content onto multiple lines.
566    Wrap,
567}
568
569impl TableOverflow {
570    /// Parses the table-cell overflow policy accepted by config and flags.
571    ///
572    /// # Examples
573    ///
574    /// ```
575    /// use osp_cli::ui::TableOverflow;
576    ///
577    /// assert_eq!(TableOverflow::parse("wrap"), Some(TableOverflow::Wrap));
578    /// assert_eq!(TableOverflow::parse("truncate"), Some(TableOverflow::Ellipsis));
579    /// assert_eq!(TableOverflow::parse("wat"), None);
580    /// ```
581    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/// Border style applied to rendered tables.
593#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
594pub enum TableBorderStyle {
595    /// Render tables without outer borders.
596    None,
597    /// Render tables with square box-drawing borders.
598    #[default]
599    Square,
600    /// Render tables with rounded box-drawing borders.
601    Round,
602}
603
604impl TableBorderStyle {
605    /// Parses the table border chrome accepted by config and flags.
606    ///
607    /// # Examples
608    ///
609    /// ```
610    /// use osp_cli::ui::TableBorderStyle;
611    ///
612    /// assert_eq!(TableBorderStyle::parse("box"), Some(TableBorderStyle::Square));
613    /// assert_eq!(TableBorderStyle::parse("rounded"), Some(TableBorderStyle::Round));
614    /// assert_eq!(TableBorderStyle::parse("wat"), None);
615    /// ```
616    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/// Border style override for help tables.
627#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
628pub enum HelpTableChrome {
629    /// Reuse the normal table border style.
630    Inherit,
631    /// Render help tables without box chrome.
632    #[default]
633    None,
634    /// Render help tables with square box-drawing borders.
635    Square,
636    /// Render help tables with rounded box-drawing borders.
637    Round,
638}
639
640impl HelpTableChrome {
641    /// Parses the help-table chrome override accepted by config.
642    ///
643    /// # Examples
644    ///
645    /// ```
646    /// use osp_cli::ui::HelpTableChrome;
647    ///
648    /// assert_eq!(HelpTableChrome::parse("inherit"), Some(HelpTableChrome::Inherit));
649    /// assert_eq!(HelpTableChrome::parse("plain"), Some(HelpTableChrome::None));
650    /// assert_eq!(HelpTableChrome::parse("wat"), None);
651    /// ```
652    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    /// Resolves the concrete help-table border after applying the override.
663    ///
664    /// # Examples
665    ///
666    /// ```
667    /// use osp_cli::ui::{HelpTableChrome, TableBorderStyle};
668    ///
669    /// assert_eq!(
670    ///     HelpTableChrome::Inherit.resolve(TableBorderStyle::Round),
671    ///     TableBorderStyle::Round
672    /// );
673    /// assert_eq!(
674    ///     HelpTableChrome::Square.resolve(TableBorderStyle::None),
675    ///     TableBorderStyle::Square
676    /// );
677    /// ```
678    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    /// Starts building render settings from the default UI baseline.
690    ///
691    /// Prefer this for real caller-facing render policy. For deterministic
692    /// docs/tests, [`RenderSettings::test_plain`] is usually a better starting
693    /// point because it disables ANSI and rich terminal inference up front.
694    pub fn builder() -> RenderSettingsBuilder {
695        RenderSettingsBuilder::new()
696    }
697
698    /// Shared plain-mode baseline for deterministic tests and examples.
699    ///
700    /// This keeps docs and tests from duplicating a large struct literal every
701    /// time they need a stable no-color rendering baseline.
702    ///
703    /// # Examples
704    ///
705    /// ```
706    /// use osp_cli::core::output::OutputFormat;
707    /// use osp_cli::ui::{RenderBackend, RenderSettings};
708    ///
709    /// let resolved = RenderSettings::test_plain(OutputFormat::Json)
710    ///     .resolve_render_settings();
711    ///
712    /// assert_eq!(resolved.backend, RenderBackend::Plain);
713    /// assert!(!resolved.color);
714    /// assert!(!resolved.unicode);
715    /// ```
716    pub fn test_plain(format: OutputFormat) -> Self {
717        RenderSettingsBuilder::plain(format).build()
718    }
719
720    /// Returns whether guide output should be preferred for the current
721    /// settings.
722    ///
723    /// This only falls back to guide mode when the caller did not explicitly
724    /// request another format.
725    ///
726    /// # Examples
727    ///
728    /// ```
729    /// use osp_cli::core::output::OutputFormat;
730    /// use osp_cli::ui::{GuideDefaultFormat, RenderSettings};
731    ///
732    /// let mut settings = RenderSettings::test_plain(OutputFormat::Auto);
733    /// settings.format_explicit = false;
734    /// settings.guide_default_format = GuideDefaultFormat::Guide;
735    /// assert!(settings.prefers_guide_rendering());
736    ///
737    /// settings.format_explicit = true;
738    /// settings.format = OutputFormat::Json;
739    /// assert!(!settings.prefers_guide_rendering());
740    /// ```
741    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
748/// Renders rows using the configured output format.
749///
750/// # Examples
751///
752/// ```
753/// use osp_cli::core::output::OutputFormat;
754/// use osp_cli::row;
755/// use osp_cli::ui::{RenderSettings, render_rows};
756///
757/// let rendered = render_rows(
758///     &[row! { "uid" => "alice" }],
759///     &RenderSettings::test_plain(OutputFormat::Json),
760/// );
761///
762/// assert!(rendered.contains("\"uid\": \"alice\""));
763/// ```
764pub 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
775/// Renders a structured output result using the configured output format.
776///
777/// # Examples
778///
779/// ```
780/// use osp_cli::core::output::OutputFormat;
781/// use osp_cli::core::output_model::OutputResult;
782/// use osp_cli::row;
783/// use osp_cli::ui::{RenderSettings, render_output};
784///
785/// let output = OutputResult::from_rows(vec![row! { "uid" => "alice" }]);
786/// let rendered = render_output(&output, &RenderSettings::test_plain(OutputFormat::Table));
787///
788/// assert!(rendered.contains("uid"));
789/// assert!(rendered.contains("alice"));
790/// ```
791pub 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
900/// Renders a document directly with the resolved UI settings.
901///
902/// # Examples
903///
904/// ```
905/// use osp_cli::core::output::OutputFormat;
906/// use osp_cli::ui::{Block, Document, LineBlock, LinePart, RenderSettings, render_document};
907///
908/// let document = Document {
909///     blocks: vec![Block::Line(LineBlock {
910///         parts: vec![LinePart {
911///             text: "hello".to_string(),
912///             token: None,
913///         }],
914///     })],
915/// };
916/// let rendered = render_document(&document, &RenderSettings::test_plain(OutputFormat::Auto));
917///
918/// assert!(rendered.contains("hello"));
919/// ```
920pub 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
936/// Renders rows in plain copy-safe form.
937///
938/// Copy helpers intentionally bypass ANSI and rich terminal styling so the
939/// clipboard gets stable plain text.
940///
941/// # Examples
942///
943/// ```
944/// use osp_cli::core::output::OutputFormat;
945/// use osp_cli::row;
946/// use osp_cli::ui::{RenderSettings, render_rows_for_copy};
947///
948/// let rendered = render_rows_for_copy(
949///     &[row! { "uid" => "alice" }],
950///     &RenderSettings::test_plain(OutputFormat::Json),
951/// );
952///
953/// assert!(rendered.contains("\"uid\": \"alice\""));
954/// ```
955pub 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
966/// Renders an output result in plain copy-safe form.
967///
968/// This keeps the caller's format preference where possible, but forces the
969/// effective render settings onto a plain-text path so copied output does not
970/// contain ANSI escapes or terminal-only chrome.
971///
972/// # Examples
973///
974/// ```
975/// use osp_cli::core::output::{ColorMode, OutputFormat};
976/// use osp_cli::core::output_model::OutputResult;
977/// use osp_cli::row;
978/// use osp_cli::ui::{RenderSettings, render_output_for_copy};
979///
980/// let output = OutputResult::from_rows(vec![row! { "uid" => "alice" }]);
981/// let settings = RenderSettings::builder()
982///     .with_format(OutputFormat::Json)
983///     .with_color(ColorMode::Always)
984///     .build();
985/// let rendered = render_output_for_copy(&output, &settings);
986///
987/// assert!(rendered.contains("\"uid\": \"alice\""));
988/// assert!(!rendered.contains('\u{1b}'));
989/// ```
990pub 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
1002/// Renders a document in plain copy-safe form.
1003///
1004/// Like [`render_output_for_copy`], this strips ANSI/rich rendering concerns so
1005/// the returned text is suitable for clipboards and paste targets.
1006///
1007/// # Examples
1008///
1009/// ```
1010/// use osp_cli::core::output::{ColorMode, OutputFormat};
1011/// use osp_cli::ui::{
1012///     Block, Document, LineBlock, LinePart, RenderSettings, render_document_for_copy,
1013/// };
1014///
1015/// let document = Document {
1016///     blocks: vec![Block::Line(LineBlock {
1017///         parts: vec![LinePart {
1018///             text: "hello".to_string(),
1019///             token: None,
1020///         }],
1021///     })],
1022/// };
1023/// let settings = RenderSettings::builder()
1024///     .with_format(OutputFormat::Auto)
1025///     .with_color(ColorMode::Always)
1026///     .build();
1027/// let rendered = render_document_for_copy(&document, &settings);
1028///
1029/// assert_eq!(rendered.trim(), "hello");
1030/// assert!(!rendered.contains('\u{1b}'));
1031/// ```
1032pub 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
1038/// Copies rendered rows to the configured clipboard service.
1039///
1040/// This is a shorthand for wrapping `rows` in an [`OutputResult`] and then
1041/// calling [`copy_output_to_clipboard`].
1042pub 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
1058/// Copies rendered output to the configured clipboard service.
1059///
1060/// The output is first rendered through [`render_output_for_copy`], so the
1061/// clipboard receives plain text rather than ANSI-rich terminal output.
1062///
1063/// # Examples
1064///
1065/// ```no_run
1066/// use osp_cli::core::output::OutputFormat;
1067/// use osp_cli::core::output_model::OutputResult;
1068/// use osp_cli::row;
1069/// use osp_cli::ui::{ClipboardService, RenderSettings, copy_output_to_clipboard};
1070///
1071/// let clipboard = ClipboardService::new().with_osc52(false);
1072/// let output = OutputResult::from_rows(vec![row! { "uid" => "alice" }]);
1073///
1074/// copy_output_to_clipboard(
1075///     &output,
1076///     &RenderSettings::test_plain(OutputFormat::Json),
1077///     &clipboard,
1078/// )?;
1079/// # Ok::<(), osp_cli::ui::ClipboardError>(())
1080/// ```
1081pub 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}