Skip to content

Commit 68b413d

Browse files
committed
Log output mode is now finished and polished.
1 parent 4df5278 commit 68b413d

File tree

3 files changed

+102
-789
lines changed

3 files changed

+102
-789
lines changed

rust/portablemc-cli/src/output.rs

Lines changed: 102 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
//! Various utilities to ease outputting human or machine readable text.
22
3-
use std::io::{IsTerminal, StdoutLock, Write};
3+
use std::fmt::{Display, Write as _};
44
use std::time::{Duration, Instant};
5-
use std::fmt::Display;
5+
use std::io::{IsTerminal, Write};
66
use std::{env, io};
77

88

@@ -12,8 +12,10 @@ use std::{env, io};
1212
pub struct Output {
1313
/// Mode-specific data.
1414
mode: OutputMode,
15-
/// Is color enabled or disabled.
16-
color: bool,
15+
/// Are cursor escape code supported on stdout.
16+
escape_cursor_cap: bool,
17+
/// Are color escape code supported on stdout.
18+
escape_color_cap: bool,
1719
}
1820

1921
#[derive(Debug)]
@@ -35,16 +37,28 @@ impl Output {
3537
}
3638

3739
fn new(mode: OutputMode) -> Self {
40+
41+
let term_dumb = !io::stdout().is_terminal() || (cfg!(unix) && env::var_os("TERM").map(|term| term == "dumb").unwrap_or_default());
42+
let no_color = env::var_os("NO_COLOR").map(|s| !s.is_empty()).unwrap_or_default();
43+
3844
Self {
3945
mode,
40-
color: has_stdout_color(),
46+
escape_cursor_cap: !term_dumb,
47+
escape_color_cap: !term_dumb && !no_color,
4148
}
4249
}
4350

4451
/// Enter log mode, this is exclusive with other modes.
4552
pub fn log(&mut self) -> LogOutput {
53+
54+
// Save the initial cursor position for the first line to be written.
55+
if self.escape_cursor_cap {
56+
print!("\x1b[s");
57+
}
58+
4659
LogOutput {
4760
output: self,
61+
shared: LogShared::default(),
4862
}
4963
}
5064

@@ -61,45 +75,47 @@ impl Output {
6175
pub struct LogOutput<'a> {
6276
/// Exclusive access to output.
6377
output: &'a mut Output,
78+
/// Buffer storing the current background log message.
79+
shared: LogShared,
6480
}
6581

66-
impl<'o> LogOutput<'o> {
82+
/// Internal buffer for the current line.
83+
#[derive(Debug, Default)]
84+
struct LogShared {
85+
/// Line buffer that will be printed when the log is dropped.
86+
line: String,
87+
/// For human-readable only, storing the rendered background log.
88+
background: String
89+
}
6790

68-
/// Log an information with a simple code referencing it.
69-
pub fn log(&mut self, code: &str) -> Log<'_, false> {
91+
impl<'o> LogOutput<'o> {
7092

71-
let mut writer = io::stdout().lock();
93+
fn _log<const BG: bool>(&mut self, code: &str) -> Log<'_, BG> {
7294

7395
if let OutputMode::TabSeparated { } = self.output.mode {
74-
write!(writer, "{code}").unwrap();
96+
debug_assert!(self.shared.line.is_empty());
97+
self.shared.line.push_str(code);
7598
}
7699

77100
Log {
78101
output: &mut self.output,
79-
writer,
80-
has_message: false,
102+
shared: &mut self.shared,
81103
}
82104

83105
}
84106

107+
/// Log an information with a simple code referencing it.
108+
#[inline]
109+
pub fn log(&mut self, code: &str) -> Log<'_, false> {
110+
self._log(code)
111+
}
112+
85113
/// A special log type that is interpreted as a background task, on machine readable
86114
/// outputs it acts as a regular log, but on human-readable outputs it will be
87115
/// displayed at the end of the current line.
88116
#[inline]
89117
pub fn background_log(&mut self, code: &str) -> Log<'_, true> {
90-
91-
let mut writer = io::stdout().lock();
92-
93-
if let OutputMode::TabSeparated { } = self.output.mode {
94-
write!(writer, "{code}").unwrap();
95-
}
96-
97-
Log {
98-
output: &mut self.output,
99-
writer,
100-
has_message: false,
101-
}
102-
118+
self._log(code)
103119
}
104120

105121
}
@@ -109,18 +125,21 @@ impl<'o> LogOutput<'o> {
109125
pub struct Log<'a, const BG: bool> {
110126
/// Exclusive access to output.
111127
output: &'a mut Output,
112-
/// Locked writer.
113-
writer: StdoutLock<'static>,
114-
/// Set to true after the first human-readable message was written.
115-
has_message: bool,
128+
/// Internal buffer.
129+
shared: &'a mut LogShared,
116130
}
117131

118132
impl<const BG: bool> Log<'_, BG> {
119133

134+
// Reminder:
135+
// \x1b[s save current cursor position
136+
// \x1b[u restore saved cursor position
137+
// \x1b[K clear the whole line
138+
120139
/// Append an argument for machine-readable output.
121140
pub fn arg<D: Display>(&mut self, arg: D) -> &mut Self {
122141
if let OutputMode::TabSeparated { } = self.output.mode {
123-
write!(self.writer, "\t{arg}").unwrap();
142+
write!(self.shared.line, "\t{arg}").unwrap();
124143
}
125144
self
126145
}
@@ -133,12 +152,45 @@ impl<const BG: bool> Log<'_, BG> {
133152
{
134153
if let OutputMode::TabSeparated { } = self.output.mode {
135154
for arg in args {
136-
write!(self.writer, "\t{arg}").unwrap();
155+
write!(self.shared.line, "\t{arg}").unwrap();
137156
}
138157
}
139158
self
140159
}
141160

161+
/// Internal function to flush the line and background buffers (only relevant in
162+
/// human-readable mode)
163+
fn flush_line_background(&mut self, newline: bool) {
164+
165+
let mut lock = io::stdout().lock();
166+
167+
if self.output.escape_cursor_cap {
168+
// If supporting cursor escape code, we don't use carriage return but instead
169+
// we use cursor save/restore position in order to easily support wrapping.
170+
lock.write_all(b"\x1b[u\x1b[K").unwrap();
171+
} else {
172+
lock.write_all(b"\r").unwrap();
173+
}
174+
175+
lock.write_all(self.shared.line.as_bytes()).unwrap();
176+
lock.write_all(self.shared.background.as_bytes()).unwrap();
177+
178+
if newline {
179+
180+
self.shared.line.clear();
181+
self.shared.background.clear();
182+
183+
lock.write_all(b"\n").unwrap();
184+
if self.output.escape_cursor_cap {
185+
lock.write_all(b"\x1b[s").unwrap();
186+
}
187+
188+
}
189+
190+
lock.flush().unwrap();
191+
192+
}
193+
142194
}
143195

144196
impl Log<'_, false> {
@@ -160,20 +212,14 @@ impl Log<'_, false> {
160212
LogLevel::Error => ("FAILED", "\x1b[31m"),
161213
};
162214

163-
// \r got to line start
164-
// \x1b[K clear the whole line
165-
if !self.output.color || color.is_empty() {
166-
write!(self.writer, "\r\x1b[K[{name:^6}] {message}").unwrap();
215+
self.shared.line.clear();
216+
if !self.output.escape_color_cap || color.is_empty() {
217+
write!(self.shared.line, "[{name:^6}] {message}").unwrap();
167218
} else {
168-
write!(self.writer, "\r\x1b[K[{color}{name:^6}\x1b[0m] {message}").unwrap();
219+
write!(self.shared.line, "[{color}{name:^6}\x1b[0m] {message}").unwrap();
169220
}
170221

171-
// If not a progress level, do a line return.
172-
if level != LogLevel::Progress {
173-
self.writer.write_all(b"\n").unwrap();
174-
}
175-
176-
self.has_message = true;
222+
self.flush_line_background(level != LogLevel::Progress);
177223

178224
}
179225
}
@@ -213,8 +259,12 @@ impl Log<'_, true> {
213259
/// overwrite any background message currently written on the current log line.
214260
pub fn message<D: Display>(&mut self, message: D) -> &mut Self {
215261
if let OutputMode::Human { .. } = self.output.mode {
216-
// \x1b[u: restore saved cursor position
217-
write!(self.writer, "\x1b[u{message}").unwrap();
262+
263+
self.shared.background.clear();
264+
write!(self.shared.background, "{message}").unwrap();
265+
266+
self.flush_line_background(false);
267+
218268
}
219269
self
220270
}
@@ -229,17 +279,17 @@ impl<const BACKGROUND: bool> Drop for Log<'_, BACKGROUND> {
229279
// Save the position of the cursor at the end of the line, this is used to
230280
// easily rewrite the background task.
231281
if let OutputMode::Human { .. } = self.output.mode {
232-
if !BACKGROUND && self.has_message {
233-
// \x1b[s save current cursor position
234-
self.writer.write_all(b"\x1b[s").unwrap();
235-
}
282+
// Do nothing in human mode because the message is always immediately
283+
// flushed to stdout, the buffers may not be empty because if we don't
284+
// add a newline then the buffer is kept for being rewritten on next log.
236285
} else {
237-
// Not in human-readable mode, line return anyway.
238-
self.writer.write_all(b"\n").unwrap();
286+
// Not in human-readable mode, the buffer has not already been flushed.
287+
let mut lock = io::stdout().lock();
288+
lock.write_all(self.shared.line.as_bytes()).unwrap();
289+
lock.write_all(b"\n").unwrap();
290+
lock.flush().unwrap();
239291
}
240292

241-
self.writer.flush().unwrap();
242-
243293
}
244294
}
245295

@@ -314,31 +364,6 @@ impl DownloadTracker {
314364

315365
}
316366

317-
318-
/// Return true if color should be used on terminal.
319-
///
320-
/// Supporting `NO_COLOR` (https://no-color.org/) and `TERM=dumb`.
321-
fn has_color() -> bool {
322-
if cfg!(unix) && env::var_os("TERM").map(|term| term == "dumb").unwrap_or_default() {
323-
false
324-
} else if env::var_os("NO_COLOR").map(|s| !s.is_empty()).unwrap_or_default() {
325-
false
326-
} else {
327-
true
328-
}
329-
}
330-
331-
/// Return true if color can be printed to stdout.
332-
///
333-
/// See [`has_color()`].
334-
fn has_stdout_color() -> bool {
335-
if !io::stdout().is_terminal() {
336-
false
337-
} else {
338-
has_color()
339-
}
340-
}
341-
342367
/// Find the SI unit of a given number and return the number scaled down to that unit.
343368
pub fn number_si_unit(num: f32) -> (f32, char) {
344369
match num {
@@ -348,61 +373,3 @@ pub fn number_si_unit(num: f32) -> (f32, char) {
348373
_ => (num / 1_000_000_000.0, 'G'),
349374
}
350375
}
351-
352-
// /// Compute terminal display length of a given string.
353-
// fn terminal_width(s: &str) -> usize {
354-
355-
// #[derive(Debug, Clone, Copy, PartialEq, Eq)]
356-
// enum Control {
357-
// None,
358-
// Escape,
359-
// Csi,
360-
// }
361-
362-
// let mut width = 0;
363-
// let mut control = Control::None;
364-
365-
// for ch in s.chars() {
366-
// match (control, ch) {
367-
// (Control::None, '\x1b') => {
368-
// control = Control::Escape;
369-
// }
370-
// (Control::None, c) if !c.is_control() => {
371-
// width += 1;
372-
// }
373-
// (Control::Escape, '[') => {
374-
// control = Control::Csi;
375-
// }
376-
// (Control::Escape, _) => {
377-
// control = Control::None;
378-
// }
379-
// (Control::Csi, c) if c.is_alphabetic() => {
380-
// // After a CSI control any alphabetic char is terminating the sequence.
381-
// control = Control::None;
382-
// }
383-
// _ => {}
384-
// }
385-
// }
386-
387-
// width
388-
389-
// }
390-
391-
392-
#[cfg(test)]
393-
mod tests {
394-
395-
use super::*;
396-
397-
#[test]
398-
fn check_terminal_width() {
399-
assert_eq!(terminal_width(""), 0);
400-
assert_eq!(terminal_width("\x1b"), 0);
401-
assert_eq!(terminal_width("\x1b[92m"), 0);
402-
assert_eq!(terminal_width("\x1b[92mOK"), 2);
403-
assert_eq!(terminal_width("[ \x1b[92mOK"), 5);
404-
assert_eq!(terminal_width("[ \x1b[92mOK ]"), 8);
405-
assert_eq!(terminal_width("[ \x1b[92mOK \x1b[0m]"), 8);
406-
}
407-
408-
}

0 commit comments

Comments
 (0)