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