Skip to main content

rich/
file_proxy.rs

1//! Port of upstream `rich/file_proxy.py`.
2//!
3//! Upstream's `FileProxy` stands in for `sys.stdout` or `sys.stderr` while a
4//! `Live` display runs: whole lines written to it are decoded from ANSI and
5//! printed through the console, so they land above the display instead of
6//! tearing it, and a partial line waits in a buffer for its newline.
7//!
8//! Rust cannot swap the process's standard streams the way Python reassigns
9//! `sys.stdout`: `print!` and [`std::io::stdout`] always reach file
10//! descriptor 1, and redirecting that descriptor needs unsafe, platform
11//! specific calls that would also capture the display's own output. So a
12//! [`FileProxy`] is a writer the program writes to *instead of* stdout
13//! (see [`Live::stdout`](crate::live::Live::stdout) and
14//! [`AutoLive::stdout`](crate::live::AutoLive::stdout)). Everything else
15//! follows upstream: the line buffering, the ANSI decoding, `flush` printing
16//! a partial line as console markup, and writes going straight to the
17//! proxied file whenever the display is not redirecting (it is stopped,
18//! redirection is off, or the console is not a terminal).
19
20use std::io::{self, Write};
21
22use crate::ansi::AnsiDecoder;
23use crate::text::Text;
24
25/// Which standard stream a [`FileProxy`] stands in for. Upstream's
26/// `redirect_stdout` and `redirect_stderr` switch each one separately.
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
28pub enum ProxyStream {
29    /// Standard output (`sys.stdout`).
30    Stdout,
31    /// Standard error (`sys.stderr`).
32    Stderr,
33}
34
35/// A live display a [`FileProxy`] can print through.
36///
37/// Implemented for `&mut Live<W>` and for the [`LiveHandle`] of a display on
38/// a refresh thread. Each method returns `false` when the display is not
39/// redirecting `stream`, and the proxy then writes to its file instead.
40///
41/// [`LiveHandle`]: crate::live::LiveHandle
42pub trait ProxyTarget {
43    /// Upstream's `FileProxy.write`: buffer `bytes` and print every completed
44    /// line through the display.
45    fn proxy_write(&mut self, stream: ProxyStream, bytes: &[u8]) -> bool;
46
47    /// Upstream's `FileProxy.flush`: print a buffered partial line.
48    fn proxy_flush(&mut self, stream: ProxyStream) -> bool;
49}
50
51/// A writer that sends text through a live display. Mirrors
52/// `rich.file_proxy.FileProxy`; the line buffer lives in the display, so a
53/// partial line survives from one proxy to the next, as it does on the one
54/// `sys.stdout` upstream.
55pub struct FileProxy<T: ProxyTarget, F: Write> {
56    target: T,
57    stream: ProxyStream,
58    file: F,
59}
60
61impl<T: ProxyTarget, F: Write> FileProxy<T, F> {
62    /// A proxy for `stream` that prints through `target`, writing to `file`
63    /// when the display is not redirecting.
64    pub fn new(target: T, stream: ProxyStream, file: F) -> Self {
65        FileProxy {
66            target,
67            stream,
68            file,
69        }
70    }
71
72    /// The proxied file. Port of `FileProxy.rich_proxied_file`; Python's
73    /// `fileno` and `isatty` forward to it, which here is the caller's to do.
74    pub fn rich_proxied_file(&self) -> &F {
75        &self.file
76    }
77
78    /// Which stream this proxy stands in for.
79    pub fn stream(&self) -> ProxyStream {
80        self.stream
81    }
82
83    /// Consume the proxy, returning the proxied file.
84    pub fn into_inner(self) -> F {
85        self.file
86    }
87}
88
89impl<T: ProxyTarget, F: Write> Write for FileProxy<T, F> {
90    fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
91        if self.target.proxy_write(self.stream, bytes) {
92            Ok(bytes.len())
93        } else {
94            self.file.write(bytes)
95        }
96    }
97
98    fn flush(&mut self) -> io::Result<()> {
99        if self.target.proxy_flush(self.stream) {
100            Ok(())
101        } else {
102            self.file.flush()
103        }
104    }
105}
106
107/// The state upstream keeps on each `FileProxy`: the partial line and the
108/// ANSI decoder, whose style carries from one line to the next.
109#[derive(Default)]
110pub(crate) struct ProxyBuffer {
111    pending: Vec<u8>,
112    decoder: AnsiDecoder,
113}
114
115impl ProxyBuffer {
116    /// Upstream's `write`: split off every completed line (the buffer joined
117    /// to the first), keep the rest, and return the completed lines decoded
118    /// and joined with newlines, or `None` when no line was completed.
119    pub(crate) fn write(&mut self, bytes: &[u8]) -> Option<Text> {
120        let mut lines = Vec::new();
121        let mut rest = bytes;
122        while let Some(end) = rest.iter().position(|&byte| byte == b'\n') {
123            let mut line = std::mem::take(&mut self.pending);
124            line.extend_from_slice(&rest[..end]);
125            lines.push(String::from_utf8_lossy(&line).into_owned());
126            rest = &rest[end + 1..];
127        }
128        self.pending.extend_from_slice(rest);
129        if lines.is_empty() {
130            return None;
131        }
132        let decoded: Vec<Text> = lines
133            .iter()
134            .map(|line| self.decoder.decode_line(line))
135            .collect();
136        Some(Text::new("\n").join(&decoded))
137    }
138
139    /// Upstream's `flush`: take the partial line, if any (printed as a `str`,
140    /// so as console markup).
141    pub(crate) fn flush(&mut self) -> Option<String> {
142        let pending = std::mem::take(&mut self.pending);
143        (!pending.is_empty()).then(|| String::from_utf8_lossy(&pending).into_owned())
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150
151    #[test]
152    fn buffer_splits_lines_like_upstream() {
153        let mut buffer = ProxyBuffer::default();
154        assert!(buffer.write(b"part").is_none());
155        let text = buffer.write(b"ial\nline2\nrest").expect("two lines");
156        assert_eq!(text.plain(), "partial\nline2");
157        assert_eq!(buffer.flush().as_deref(), Some("rest"));
158        assert_eq!(buffer.flush(), None);
159    }
160
161    /// A target that never redirects, as a stopped display.
162    struct Stopped;
163
164    impl ProxyTarget for Stopped {
165        fn proxy_write(&mut self, _: ProxyStream, _: &[u8]) -> bool {
166            false
167        }
168        fn proxy_flush(&mut self, _: ProxyStream) -> bool {
169            false
170        }
171    }
172
173    #[test]
174    fn a_stopped_display_writes_to_the_file() {
175        let mut proxy = FileProxy::new(Stopped, ProxyStream::Stdout, Vec::new());
176        writeln!(proxy, "plain").unwrap();
177        proxy.flush().unwrap();
178        assert_eq!(proxy.into_inner(), b"plain\n");
179    }
180}