rs-rich 0.0.9

A faithful Rust port of the Python `rich` terminal-rendering library
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
//! Rendering & extension protocols.
//!
//! Port of upstream `rich/protocol.py` + `rich/abc.py` + the highlighter
//! interface. **These traits are the sanctioned extension points of the port.**
//! Extensions in `rich-ext` (and, later, third-party plugins) implement them;
//! the faithful core only ever ships upstream's built-in implementations. See
//! docs/PLUGINS.md.

use crate::console::{Console, ConsoleOptions};
use crate::measure::Measurement;
use crate::segment::Segment;
use crate::text::Text;

/// Anything that can be rendered to a stream of [`Segment`]s within a width.
///
/// The Rust equivalent of upstream's `__rich_console__(console, options)`
/// protocol. Implement it to make a custom type printable by [`Console`]. The
/// `options` carry the available width (and, later, height/justify) the
/// renderable must fit into. Newlines between lines are emitted as ordinary
/// segments containing `\n`.
pub trait Renderable {
    fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment>;

    /// The `(minimum, maximum)` cell width this renderable wants. The default
    /// assumes the renderable fills the available width (e.g. `Panel`, `Table`);
    /// `Text` overrides it with its content width so the top-level print path can
    /// shrink to fit. Port of `__rich_measure__` / `Measurement.get`.
    fn measure(&self, _console: &Console, options: &ConsoleOptions) -> Measurement {
        Measurement::new(options.max_width, options.max_width)
    }

    /// Whether a top-level `Console::print` shrinks this renderable to its
    /// measured width. Upstream renders every top-level renderable at the full
    /// console width, so the default is `false`; an extension may opt in.
    fn fit_to_measurement(&self) -> bool {
        false
    }

    /// The `Text` a top-level print renders in place of this renderable.
    ///
    /// Upstream's `Console._collect_renderables` rebuilds printed `str`/`Text`
    /// values through `Text(sep, end=end).join(...)`, whose `blank_copy` takes
    /// `justify`, `overflow` and `no_wrap` from the separator. Only `Text`
    /// overrides this.
    #[doc(hidden)]
    fn printed_text(&self) -> Option<Text> {
        None
    }

    /// The vertical alignment a `Table` cell holding this renderable uses in
    /// place of its column's. Upstream reads `getattr(renderable, "vertical",
    /// None)`; [`Align`](crate::align::Align) sets it.
    fn vertical(&self) -> Option<crate::align::VerticalAlign> {
        None
    }
}

/// Optional line-streaming extension point for renderables.
///
/// Mirrors the incremental consumption of upstream's rendering generators.
/// Consumers can write each visual line immediately instead of collecting the
/// complete segment stream. Implementations may still retain source data for
/// measurement. This trait keeps streaming hooks out of inherent core APIs.
pub trait LineRenderable: Renderable {
    /// Emit styled visual lines without trailing newlines, stopping immediately
    /// on the callback's first error. An empty segment represents a blank line;
    /// calling the callback zero times represents no output.
    fn try_for_each_line<E>(
        &self,
        console: &Console,
        options: &ConsoleOptions,
        emit: impl FnMut(Vec<Segment>) -> Result<(), E>,
    ) -> Result<(), E>;
}

/// Transfer already-owned table rows without cloning every cell string.
///
/// This extension point changes ownership only. Column definitions, measurement
/// and rendering follow the table's existing rules, including missing/extra cells.
/// Producers that parse into owned strings can release their row collection as
/// they populate a table instead of retaining a second complete copy.
pub trait OwnedTableRows {
    fn extend_owned_rows(&mut self, rows: Vec<Vec<String>>) -> &mut Self;
}

/// A transformer that adds style spans to [`Text`] (e.g. syntax/number/URL
/// highlighting). The Rust equivalent of upstream's `Highlighter` ABC.
///
/// This is the primary *plugin* seam for the first slice: `rich-ext` registers
/// [`Highlighter`]s onto a [`Console`] without the core knowing they exist.
pub trait Highlighter {
    /// Inspect `text` and apply any style spans in place.
    fn highlight(&self, text: &mut Text);
}

/// One styled byte range of a highlighted line.
#[derive(Clone, Debug, PartialEq)]
pub struct HighlightSpan {
    /// Byte range within the line (the line's text, without its `\n`).
    pub range: std::ops::Range<usize>,
    pub style: crate::style::Style,
}

/// The spans of one source line, plus the style of the line break after it.
#[derive(Clone, Debug, Default, PartialEq)]
pub struct HighlightedLine {
    /// Sorted, non-overlapping spans. Bytes no span covers take
    /// [`HighlightedCode::default_style`].
    pub spans: Vec<HighlightSpan>,
    /// The style an engine gives the `\n` ending this line (for example, inside a
    /// multi-line string). `None` when the engine does not style line breaks.
    pub newline_style: Option<crate::style::Style>,
}

/// What a [`CodeHighlighter`] returns: one [`HighlightedLine`] per element of
/// `code.split('\n')`, so a trailing newline yields a final empty line.
#[derive(Clone, Debug, Default, PartialEq)]
pub struct HighlightedCode {
    pub lines: Vec<HighlightedLine>,
    /// The theme's background, if it has one. `Syntax` paints its block with it.
    pub background: Option<crate::color::Color>,
    /// The style for text no span covers.
    pub default_style: crate::style::Style,
}

/// Why a [`CodeHighlighter`] could not highlight. An unknown *language* is not an
/// error: highlighters fall back to plain text.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum HighlightError {
    /// The theme name is not one this highlighter provides.
    UnknownTheme(String),
    /// The engine failed on this input.
    Engine(String),
}

impl std::fmt::Display for HighlightError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            HighlightError::UnknownTheme(name) => write!(f, "unknown syntax theme {name:?}"),
            HighlightError::Engine(message) => write!(f, "syntax highlighting failed: {message}"),
        }
    }
}

impl std::error::Error for HighlightError {}

/// A syntax-highlighting engine behind [`Syntax`](crate::syntax::Syntax) and
/// Markdown code blocks.
///
/// Upstream highlights with Pygments; the port's default is
/// [`SyntectHighlighter`](crate::syntax::SyntectHighlighter), and anything that
/// implements this trait can replace it (see [`Syntax::highlighter`](crate::syntax::Syntax::highlighter)).
///
/// # Contract
///
/// - `lines` has one entry per element of `code.split('\n')`.
/// - Spans are sorted, do not overlap, stay inside their line and start and end
///   on UTF-8 character boundaries.
/// - An unknown language highlights as plain text rather than failing.
///
/// Core validates what an implementation returns: out-of-range, overlapping or
/// misaligned spans are dropped, missing lines render unstyled, and span styles
/// lose any hyperlink. The rendered characters always come from the source, so
/// a highlighter cannot add text or terminal control sequences.
///
/// ```
/// use rich::protocol::{CodeHighlighter, HighlightError, HighlightSpan, HighlightedCode, HighlightedLine};
/// use rich::Style;
///
/// /// Makes every line bold.
/// struct Bold;
///
/// impl CodeHighlighter for Bold {
///     fn highlight(&self, code: &str, _language: Option<&str>, _theme: &str)
///         -> Result<HighlightedCode, HighlightError>
///     {
///         let bold = Style::parse("bold").unwrap();
///         let lines = code
///             .split('\n')
///             .map(|line| HighlightedLine {
///                 spans: vec![HighlightSpan { range: 0..line.len(), style: bold.clone() }]
///                     .into_iter()
///                     .filter(|span| !span.range.is_empty())
///                     .collect(),
///                 newline_style: None,
///             })
///             .collect();
///         Ok(HighlightedCode { lines, ..Default::default() })
///     }
///     fn default_theme(&self) -> &str { "bold" }
///     fn themes(&self) -> Vec<String> { vec!["bold".into()] }
///     fn languages(&self) -> Vec<String> { Vec::new() }
/// }
///
/// # #[cfg(feature = "syntax")] {
/// use rich::{Console, Syntax};
/// use std::sync::Arc;
///
/// let console = Console::builder().width(20).force_terminal(true).build();
/// let out = console.render_to_string(&Syntax::new("x = 1", "python").highlighter(Arc::new(Bold)));
/// assert!(out.contains("\x1b[1mx = 1"));
/// # }
/// ```
pub trait CodeHighlighter: Send + Sync {
    /// Highlight `code`. `language` is a name or file extension (`"rust"`,
    /// `"rs"`); `None` means plain text. `theme` is one of [`themes`](Self::themes).
    fn highlight(
        &self,
        code: &str,
        language: Option<&str>,
        theme: &str,
    ) -> Result<HighlightedCode, HighlightError>;

    /// The theme used when none is chosen.
    fn default_theme(&self) -> &str;

    /// Every theme name `highlight` accepts.
    fn themes(&self) -> Vec<String>;

    /// Language names this highlighter knows, for help text and completion.
    fn languages(&self) -> Vec<String>;

    /// The language for a file path, if the highlighter recognises it.
    fn language_for_path(&self, _path: &std::path::Path) -> Option<String> {
        None
    }

    /// `theme`'s style for a Pygments token type — `"Text"` or `"Comment"` —
    /// as upstream's `SyntaxTheme.get_style_for_token`. `Syntax` colours its
    /// line numbers and indent guides with it; `None` (the default) means the
    /// theme sets nothing for the token.
    fn token_style(&self, _theme: &str, _token: &str) -> Option<crate::style::Style> {
        None
    }
}

/// Renders fenced Markdown code blocks of particular languages (for example
/// ```` ```mermaid ````) in place of the usual highlighted code.
///
/// Upstream always renders a fence through `Syntax`, and so does
/// [`Markdown`](crate::markdown::Markdown) unless a renderer is added with
/// [`Markdown::fence_renderer`](crate::markdown::Markdown::fence_renderer).
/// Markdown asks each renderer in turn; the first to return `Some` wins, and if
/// none does the block is highlighted as code as before.
///
/// The fence body comes from the document, so treat it as untrusted: whatever
/// text of it an implementation echoes must not carry terminal control
/// sequences.
pub trait FenceRenderer: Send + Sync {
    /// Render the body of a fence whose info string starts with `language`, or
    /// return `None` to decline. The returned segments fit `options.max_width`
    /// and separate lines with `\n` segments, like [`Renderable::rich_render`].
    fn render_fence(
        &self,
        language: &str,
        code: &str,
        console: &Console,
        options: &ConsoleOptions,
    ) -> Option<Vec<Segment>>;
}

/// A console-wide default [`CodeHighlighter`] and the theme to use with it.
/// See [`ConsoleCodeHighlighting`].
#[derive(Clone)]
pub struct CodeHighlighting {
    pub highlighter: std::sync::Arc<dyn CodeHighlighter>,
    /// One of `highlighter`'s themes; `None` is its default theme.
    pub theme: Option<String>,
}

impl std::fmt::Debug for CodeHighlighting {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("CodeHighlighting")
            .field("default_theme", &self.highlighter.default_theme())
            .field("theme", &self.theme)
            .finish()
    }
}

/// Attach/query a console's default code highlighter.
///
/// A [`Syntax`](crate::syntax::Syntax) without a highlighter of its own, and
/// so Markdown code blocks, highlights with the console's when it renders;
/// the console's theme applies when the `Syntax` names none. Without one,
/// the default [`SyntectHighlighter`](crate::syntax::SyntectHighlighter) is
/// used, as before: upstream has no such setting.
pub trait ConsoleCodeHighlighting {
    fn set_code_highlighting(&mut self, value: Option<CodeHighlighting>);
    fn code_highlighting(&self) -> Option<&CodeHighlighting>;
}

/// Evidence for an optional output protocol; inference is not confirmation.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Support {
    Unsupported,
    Inferred,
    Confirmed,
}

/// Immutable destination capabilities supplied by an extension. No detection or I/O.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct TargetCapabilities {
    pub width: usize,
    pub height: usize,
    pub color_system: Option<crate::color::ColorSystem>,
    pub interactive: bool,
    pub unicode: bool,
    pub hyperlinks: bool,
    pub sixel: Support,
}

/// Optional context shared by nested renderables without changing their protocol.
pub trait RenderEnvironment: Send + Sync {
    fn capabilities(&self) -> TargetCapabilities;
}

/// Attach/query a per-console immutable extension environment.
pub trait ConsoleEnvironment {
    fn set_render_environment(&mut self, value: Option<std::sync::Arc<dyn RenderEnvironment>>);
    fn render_environment(&self) -> Option<&dyn RenderEnvironment>;
}

/// What a semantic region is: the role a renderable reports for the cells it
/// drew. See [`RegionSink`].
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum RegionRole {
    /// A [`Panel`](crate::panel::Panel): its border, padding and content.
    Panel,
    /// A [`Table`](crate::table::Table), title and caption included.
    Table,
    /// A header cell of column `column` (from 0).
    TableHeader { column: usize },
    /// A body cell: row `row` of the table's rows, column `column` (from 0).
    TableCell { row: usize, column: usize },
    /// A footer cell of column `column` (from 0).
    TableFooter { column: usize },
    /// A [`Rule`](crate::rule::Rule).
    Rule,
    /// A Markdown heading, `level` 1 to 6.
    Heading { level: u8 },
    /// A Markdown code block.
    Code,
    /// A hyperlink. Core reports links through [`Style::link`](crate::style::Style::link),
    /// not through a sink; consumers derive link regions from styles.
    Link,
    /// A role an extension names.
    Other(String),
}

/// A region a renderable reports: its role, and an optional label (a panel's
/// title, a heading's text) and link target.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RegionInfo {
    pub role: RegionRole,
    pub label: Option<String>,
    pub link: Option<String>,
}

impl RegionInfo {
    pub fn new(role: RegionRole) -> Self {
        RegionInfo {
            role,
            label: None,
            link: None,
        }
    }

    /// With `label`, trimmed, unless that leaves it empty.
    pub fn label(mut self, label: impl AsRef<str>) -> Self {
        let label = label.as_ref().trim();
        self.label = (!label.is_empty()).then(|| label.to_string());
        self
    }

    pub fn link(mut self, link: impl Into<String>) -> Self {
        self.link = Some(link.into());
        self
    }
}

/// A region's identity, chosen by the [`RegionSink`] that recorded it.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct RegionId(pub u64);

/// The [`Meta`](crate::style::Meta) key under which a region's id rides on the
/// styles of the segments it drew. It never renders.
pub const REGION_META_KEY: &str = "rich.region";

/// Observes semantic regions (#226): which cells a panel, table, table cell,
/// rule, Markdown heading or code block drew. Not part of upstream.
///
/// Install one with [`ConsoleRegions::set_region_sink`]. Without one (the
/// default) no renderable asks for a region, nothing is tagged, and output is
/// exactly as before. With one, a reporting renderable calls
/// [`enter`](Self::enter) before it renders its children and
/// [`exit`](Self::exit) after, so the sink sees nesting in call order. It
/// then tags every segment it drew that no child tagged, by setting
/// [`REGION_META_KEY`] in the segment's style metadata. Metadata never
/// renders, so the terminal bytes do not change; it does make styles compare
/// unequal, so a consumer that merges equal styles (HTML export) should read
/// the tags and strip them first, as `rich_ext::frame` does.
pub trait RegionSink: Send + Sync {
    /// A region starts. Return an id unique within this sink.
    fn enter(&self, region: RegionInfo) -> RegionId;
    /// The region `id` has finished rendering.
    fn exit(&self, id: RegionId);
}

/// Attach/query a console's [`RegionSink`].
pub trait ConsoleRegions {
    fn set_region_sink(&mut self, value: Option<std::sync::Arc<dyn RegionSink>>);
    fn region_sink(&self) -> Option<&dyn RegionSink>;
}

/// An entered region, exited when dropped, so an early return still exits.
pub struct RegionGuard<'a> {
    sink: &'a dyn RegionSink,
    id: RegionId,
}

impl RegionGuard<'_> {
    pub fn id(&self) -> RegionId {
        self.id
    }

    /// Tag the segments of `segments` that carry no region yet with this one.
    pub fn tag(&self, segments: &mut [Segment]) {
        tag_region(segments, self.id);
    }
}

impl Drop for RegionGuard<'_> {
    fn drop(&mut self) {
        self.sink.exit(self.id);
    }
}

/// Enter a region when `console` has a sink. With none, `None`, and `info` is
/// never called.
pub fn enter_region<'a>(
    console: &'a Console,
    info: impl FnOnce() -> RegionInfo,
) -> Option<RegionGuard<'a>> {
    let sink = console.region_sink()?;
    let id = sink.enter(info());
    Some(RegionGuard { sink, id })
}

/// `render()` as a region. Without a sink, exactly `render()`.
pub fn report_region(
    console: &Console,
    info: impl FnOnce() -> RegionInfo,
    render: impl FnOnce() -> Vec<Segment>,
) -> Vec<Segment> {
    match enter_region(console, info) {
        None => render(),
        Some(guard) => {
            let mut segments = render();
            guard.tag(&mut segments);
            segments
        }
    }
}

/// Set [`REGION_META_KEY`] to `id` on every non-control segment that has no
/// region yet. An unstyled segment gets a style holding only the tag.
pub fn tag_region(segments: &mut [Segment], id: RegionId) {
    use crate::style::{Meta, MetaValue};
    for segment in segments.iter_mut().filter(|segment| !segment.control) {
        let style = segment.style.take().unwrap_or_default();
        if region_of(&style).is_some() {
            segment.style = Some(style);
            continue;
        }
        let mut meta = style.meta_ref().cloned().unwrap_or_else(Meta::new);
        meta.insert(REGION_META_KEY, MetaValue::Int(id.0 as i64));
        segment.style = Some(style.with_meta(meta));
    }
}

/// The region `style` was tagged with, if any.
pub fn region_of(style: &crate::style::Style) -> Option<RegionId> {
    match style.meta_ref()?.get(REGION_META_KEY)? {
        crate::style::MetaValue::Int(id) => Some(RegionId(*id as u64)),
        _ => None,
    }
}