hurl/util/
term.rs

1/*
2 * Hurl (https://hurl.dev)
3 * Copyright (C) 2025 Orange
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *          http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 *
17 */
18//! Wrapper on standard output/error.
19use std::io;
20#[cfg(target_family = "windows")]
21use std::io::IsTerminal;
22use std::io::Write;
23
24/// The way to write on standard output and error: either immediate like `println!` macro,
25/// or buffered in an internal buffer.
26#[derive(Copy, Clone, Debug, Eq, PartialEq)]
27pub enum WriteMode {
28    /// Messages are printed immediately.
29    Immediate,
30    /// Messages are saved to an internal buffer, and can be retrieved with [`Stdout::buffer`] /
31    /// [`Stderr::buffer`].
32    Buffered,
33}
34
35/// Indirection for standard output.
36///
37/// Depending on `mode`, bytes are immediately printed to standard output, or buffered in an
38/// internal buffer.
39pub struct Stdout {
40    /// Write mode of the standard output: immediate or saved to a buffer.
41    mode: WriteMode,
42    /// Internal buffer, filled when `mode` is [`WriteMode::Buffered`]
43    buffer: Vec<u8>,
44}
45
46impl Stdout {
47    /// Creates a new standard output, buffered or immediate depending on `mode`.
48    pub fn new(mode: WriteMode) -> Self {
49        Stdout {
50            mode,
51            buffer: Vec::new(),
52        }
53    }
54
55    /// Attempts to write an entire buffer into standard output.
56    pub fn write_all(&mut self, buf: &[u8]) -> Result<(), io::Error> {
57        match self.mode {
58            WriteMode::Immediate => write_stdout(buf),
59            WriteMode::Buffered => self.buffer.write_all(buf),
60        }
61    }
62
63    /// Returns the buffered standard output.
64    pub fn buffer(&self) -> &[u8] {
65        &self.buffer
66    }
67}
68
69#[cfg(target_family = "unix")]
70fn write_stdout(buf: &[u8]) -> Result<(), io::Error> {
71    let mut handle = io::stdout().lock();
72    handle.write_all(buf)?;
73    Ok(())
74}
75
76#[cfg(target_family = "windows")]
77fn write_stdout(buf: &[u8]) -> Result<(), io::Error> {
78    // From <https://doc.rust-lang.org/std/io/struct.Stdout.html>:
79    // > When operating in a console, the Windows implementation of this stream does not support
80    // > non-UTF-8 byte sequences. Attempting to write bytes that are not valid UTF-8 will return
81    // > an error.
82    // As a workaround to prevent error, we convert the buffer to an UTF-8 string (with potential
83    // bytes losses) before writing to the standard output of the Windows console.
84    if io::stdout().is_terminal() {
85        println!("{}", String::from_utf8_lossy(buf));
86    } else {
87        let mut handle = io::stdout().lock();
88        handle.write_all(buf)?;
89    }
90    Ok(())
91}
92
93/// Indirection for standard error.
94///
95/// Depending on `mode`, messages are immediately printed to standard error, or buffered in an
96/// internal buffer.
97///
98/// An optional `progress` string can be used to report temporary progress indication to the user.
99/// It's always printed as the last lines of the standard error. When the standard error is created
100/// with [`WriteMode::Buffered`], the progress is not saved in the internal buffer.
101#[derive(Clone, Debug)]
102pub struct Stderr {
103    /// Write mode of the standard error: immediate or saved to a buffer.
104    mode: WriteMode,
105    /// Internal buffer, filled when `mode` is [`WriteMode::Buffered`]
106    buffer: String,
107    /// Progress bar: when not empty, it is always displayed at the end of the terminal.
108    progress_bar: String,
109}
110
111impl Stderr {
112    /// Creates a new standard error, buffered or immediate depending on `mode`.
113    pub fn new(mode: WriteMode) -> Self {
114        Stderr {
115            mode,
116            buffer: String::new(),
117            progress_bar: String::new(),
118        }
119    }
120
121    /// Returns the [`WriteMode`] of this logger.
122    pub fn mode(&self) -> WriteMode {
123        self.mode
124    }
125
126    /// Prints to the standard error, with a newline.
127    pub fn eprintln(&mut self, message: &str) {
128        match self.mode {
129            WriteMode::Immediate => {
130                let has_progress = !self.progress_bar.is_empty();
131                if has_progress {
132                    self.rewind_cursor();
133                }
134                eprintln!("{message}");
135                if has_progress {
136                    eprint!("{}", self.progress_bar);
137                }
138            }
139            WriteMode::Buffered => {
140                self.buffer.push_str(message);
141                self.buffer.push('\n');
142            }
143        }
144    }
145
146    /// Prints to the standard error.
147    pub fn eprint(&mut self, message: &str) {
148        match self.mode {
149            WriteMode::Immediate => {
150                let has_progress = !self.progress_bar.is_empty();
151                if has_progress {
152                    self.rewind_cursor();
153                }
154                eprint!("{message}");
155                if has_progress {
156                    eprint!("{}", self.progress_bar);
157                }
158            }
159            WriteMode::Buffered => {
160                self.buffer.push_str(message);
161            }
162        }
163    }
164
165    /// Sets the progress bar (only in [`WriteMode::Immediate`] mode).
166    pub fn set_progress_bar(&mut self, progress: &str) {
167        match self.mode {
168            WriteMode::Immediate => {
169                self.progress_bar = progress.to_string();
170                eprint!("{}", self.progress_bar);
171            }
172            WriteMode::Buffered => {}
173        }
174    }
175
176    /// Clears the progress string (only in [`WriteMode::Immediate`] mode).
177    pub fn clear_progress_bar(&mut self) {
178        self.rewind_cursor();
179        self.progress_bar.clear();
180    }
181
182    /// Returns the buffered standard error.
183    pub fn buffer(&self) -> &str {
184        &self.buffer
185    }
186
187    /// Set the buffered standard error.
188    pub fn set_buffer(&mut self, buffer: String) {
189        self.buffer = buffer;
190    }
191
192    /// Clears any progress and reset cursor terminal to the position of the last "real" message
193    /// (only in [`WriteMode::Immediate`] mode).
194    fn rewind_cursor(&self) {
195        if self.progress_bar.is_empty() {
196            return;
197        }
198        match self.mode {
199            WriteMode::Immediate => {
200                // We count the number of new lines \n. We can't use the `String::lines()` because
201                // it counts a line for a single char and we don't want to go up for a single char.
202                let lines = self.progress_bar.chars().filter(|c| *c == '\n').count();
203
204                // We used the following ANSI codes:
205                // - K: "EL - Erase in Line" sequence. It clears from the cursor to the end of line.
206                // - 1A: "Cursor Up". Up to one line
207                // <https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_sequences>
208                if lines > 0 {
209                    (0..lines).for_each(|_| eprint!("\x1B[1A\x1B[K"));
210                } else {
211                    eprint!("\x1B[K");
212                }
213            }
214            WriteMode::Buffered => {}
215        }
216    }
217}
218
219#[cfg(test)]
220mod tests {
221    use crate::util::term::{Stderr, Stdout, WriteMode};
222
223    #[test]
224    fn buffered_stdout() {
225        let mut stdout = Stdout::new(WriteMode::Buffered);
226        stdout.write_all(b"Hello").unwrap();
227        stdout.write_all(b" ").unwrap();
228        stdout.write_all(b"World!").unwrap();
229        assert_eq!(stdout.buffer(), b"Hello World!");
230    }
231
232    #[test]
233    fn buffered_stderr() {
234        let mut stderr = Stderr::new(WriteMode::Buffered);
235        stderr.eprintln("toto");
236        stderr.set_progress_bar("some progress...\r");
237        stderr.eprintln("tutu");
238
239        assert_eq!(stderr.buffer(), "toto\ntutu\n");
240    }
241}