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}