Skip to main content

rich_ext/
target.rs

1//! Explicit destinations for deterministic, nested rendering. No environment probes.
2use crate::frame::Frame;
3use rich::protocol::{
4    CodeHighlighting, ConsoleCodeHighlighting, ConsoleEnvironment, RenderEnvironment, Support,
5    TargetCapabilities,
6};
7use rich::{Console, Renderable, Segment, Theme};
8use std::sync::Arc;
9
10/// Output policy, independent of any writer.
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub enum TargetKind {
13    Terminal,
14    PlainStream,
15    Capture,
16    Html,
17    Svg,
18    Custom,
19}
20
21/// A complete immutable rendering context. Construct one per destination.
22#[derive(Clone, Debug)]
23pub struct RenderTarget {
24    kind: TargetKind,
25    capabilities: TargetCapabilities,
26    theme: Theme,
27    /// The default code highlighter for this target's consoles.
28    code_highlighting: Option<CodeHighlighting>,
29}
30impl RenderTarget {
31    pub fn new(kind: TargetKind, mut caps: TargetCapabilities, theme: Theme) -> Self {
32        if !matches!(kind, TargetKind::Terminal | TargetKind::Custom) {
33            caps.interactive = false;
34            caps.sixel = Support::Unsupported;
35        }
36        if kind == TargetKind::PlainStream {
37            caps.color_system = None;
38            caps.hyperlinks = false;
39        }
40        if !caps.interactive {
41            caps.sixel = Support::Unsupported;
42        }
43        Self {
44            kind,
45            capabilities: caps,
46            theme,
47            code_highlighting: None,
48        }
49    }
50    /// Give this target's consoles a default code highlighter (see
51    /// [`ConsoleCodeHighlighting`]).
52    #[must_use]
53    pub fn with_code_highlighting(mut self, highlighting: Option<CodeHighlighting>) -> Self {
54        self.code_highlighting = highlighting;
55        self
56    }
57    pub fn kind(&self) -> TargetKind {
58        self.kind
59    }
60    pub fn console(&self) -> Console {
61        let c = self.capabilities;
62        let mut console = Console::builder()
63            .width(c.width)
64            .height(c.height)
65            .force_terminal(c.interactive)
66            .color_system(c.color_system)
67            .no_color(c.color_system.is_none())
68            .ascii_only(!c.unicode)
69            .legacy_windows(false)
70            .safe_box(true)
71            .emoji(c.unicode)
72            .highlight(false)
73            .theme(self.theme.clone())
74            .build();
75        console.set_render_environment(Some(Arc::new(self.clone())));
76        console.set_code_highlighting(self.code_highlighting.clone());
77        console
78    }
79    pub fn segments(&self, value: &dyn Renderable) -> Vec<Segment> {
80        self.segments_on(&self.console(), value)
81    }
82    fn segments_on(&self, console: &Console, value: &dyn Renderable) -> Vec<Segment> {
83        let c = self.capabilities;
84        if c.width == 0 || c.height == 0 {
85            return Vec::new();
86        }
87        let mut segments = value.rich_render(
88            console,
89            &console.options().update_dimensions(c.width, c.height),
90        );
91        segments.retain(|segment| !segment.control || c.interactive);
92        if !c.hyperlinks {
93            for segment in &mut segments {
94                segment.style = segment.style.as_ref().map(|s| s.update_link(None));
95            }
96        }
97        segments
98    }
99    /// The render as a [`Frame`], with the same filtering as
100    /// [`RenderTarget::segments`]. Control segments are not content, so the
101    /// frame drops them even on an interactive target.
102    pub fn frame(&self, value: &dyn Renderable) -> Frame {
103        Frame::from_segments(&self.segments(value))
104    }
105    /// [`RenderTarget::frame`] with the semantic regions the renderables
106    /// report (see [`crate::frame::regions`]). The bytes are the same.
107    pub fn frame_with_regions(&self, value: &dyn Renderable) -> Frame {
108        self.segments_and_frame_with_regions(value).1
109    }
110    /// One render with a region sink: its segments (region tags still in
111    /// their styles) and the frame with regions built from them.
112    pub(crate) fn segments_and_frame_with_regions(
113        &self,
114        value: &dyn Renderable,
115    ) -> (Vec<Segment>, Frame) {
116        use rich::protocol::ConsoleRegions;
117        let recorder = std::sync::Arc::new(crate::frame::RegionRecorder::new());
118        let mut console = self.console();
119        console.set_region_sink(Some(recorder.clone()));
120        let segments = self.segments_on(&console, value);
121        let frame = Frame::from_segments(&segments).with_regions(&recorder);
122        (segments, frame)
123    }
124    pub fn text(&self, value: &dyn Renderable) -> String {
125        self.console().segments_to_string(&self.segments(value))
126    }
127}
128impl RenderEnvironment for RenderTarget {
129    fn capabilities(&self) -> TargetCapabilities {
130        self.capabilities
131    }
132}
133
134/// Provenance of each resolved capability, without active terminal probing.
135#[derive(Clone, Copy, Debug, PartialEq, Eq)]
136pub enum CapabilityOrigin {
137    Configured,
138    Detected,
139    Inferred,
140    Default,
141}
142#[derive(Clone, Debug)]
143pub struct DetectedCapabilities {
144    pub capabilities: TargetCapabilities,
145    pub origins: Vec<(String, CapabilityOrigin)>,
146}
147#[derive(Clone, Copy, Debug)]
148pub struct TargetObservations {
149    pub width: Option<usize>,
150    pub height: Option<usize>,
151    pub is_terminal: bool,
152    pub color_system: Option<rich::ColorSystem>,
153    pub unicode: bool,
154    pub hyperlinks: bool,
155    pub sixel_hint: Support,
156}
157#[derive(Clone, Copy, Debug, Default)]
158pub struct TargetOverrides {
159    pub width: Option<usize>,
160    pub height: Option<usize>,
161    pub interactive: Option<bool>,
162    pub color_system: Option<Option<rich::ColorSystem>>,
163    pub unicode: Option<bool>,
164    pub hyperlinks: Option<bool>,
165    pub sixel: Option<Support>,
166}
167/// Resolve caller-supplied observations and overrides. Never reads environment state.
168pub fn resolve_capabilities(
169    o: TargetObservations,
170    overrides: TargetOverrides,
171) -> DetectedCapabilities {
172    let mut origins = Vec::new();
173    fn select<T>(
174        name: &str,
175        configured: Option<T>,
176        observed: T,
177        origin: CapabilityOrigin,
178        origins: &mut Vec<(String, CapabilityOrigin)>,
179    ) -> T {
180        match configured {
181            Some(v) => {
182                origins.push((name.into(), CapabilityOrigin::Configured));
183                v
184            }
185            None => {
186                origins.push((name.into(), origin));
187                observed
188            }
189        }
190    }
191    use CapabilityOrigin::{Default, Detected, Inferred};
192    let capabilities = TargetCapabilities {
193        width: select(
194            "width",
195            overrides.width,
196            o.width.unwrap_or(80),
197            if o.width.is_some() { Detected } else { Default },
198            &mut origins,
199        ),
200        height: select(
201            "height",
202            overrides.height,
203            o.height.unwrap_or(25),
204            if o.height.is_some() {
205                Detected
206            } else {
207                Default
208            },
209            &mut origins,
210        ),
211        color_system: select(
212            "color_system",
213            overrides.color_system,
214            o.color_system,
215            Detected,
216            &mut origins,
217        ),
218        interactive: select(
219            "interactive",
220            overrides.interactive,
221            o.is_terminal,
222            Detected,
223            &mut origins,
224        ),
225        unicode: select(
226            "unicode",
227            overrides.unicode,
228            o.unicode,
229            Detected,
230            &mut origins,
231        ),
232        hyperlinks: select(
233            "hyperlinks",
234            overrides.hyperlinks,
235            o.hyperlinks,
236            Detected,
237            &mut origins,
238        ),
239        sixel: select(
240            "sixel",
241            overrides.sixel,
242            o.sixel_hint,
243            if o.sixel_hint == Support::Inferred {
244                Inferred
245            } else {
246                Detected
247            },
248            &mut origins,
249        ),
250    };
251    DetectedCapabilities {
252        capabilities,
253        origins,
254    }
255}