1
1
//! Various utilities to ease outputting human or machine readable text.
2
2
3
- use std:: io :: { IsTerminal , StdoutLock , Write } ;
3
+ use std:: fmt :: { Display , Write as _ } ;
4
4
use std:: time:: { Duration , Instant } ;
5
- use std:: fmt :: Display ;
5
+ use std:: io :: { IsTerminal , Write } ;
6
6
use std:: { env, io} ;
7
7
8
8
@@ -12,8 +12,10 @@ use std::{env, io};
12
12
pub struct Output {
13
13
/// Mode-specific data.
14
14
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 ,
17
19
}
18
20
19
21
#[ derive( Debug ) ]
@@ -35,16 +37,28 @@ impl Output {
35
37
}
36
38
37
39
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
+
38
44
Self {
39
45
mode,
40
- color : has_stdout_color ( ) ,
46
+ escape_cursor_cap : !term_dumb,
47
+ escape_color_cap : !term_dumb && !no_color,
41
48
}
42
49
}
43
50
44
51
/// Enter log mode, this is exclusive with other modes.
45
52
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
+
46
59
LogOutput {
47
60
output : self ,
61
+ shared : LogShared :: default ( ) ,
48
62
}
49
63
}
50
64
@@ -61,45 +75,47 @@ impl Output {
61
75
pub struct LogOutput < ' a > {
62
76
/// Exclusive access to output.
63
77
output : & ' a mut Output ,
78
+ /// Buffer storing the current background log message.
79
+ shared : LogShared ,
64
80
}
65
81
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
+ }
67
90
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 > {
70
92
71
- let mut writer = io :: stdout ( ) . lock ( ) ;
93
+ fn _log < const BG : bool > ( & mut self , code : & str ) -> Log < ' _ , BG > {
72
94
73
95
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) ;
75
98
}
76
99
77
100
Log {
78
101
output : & mut self . output ,
79
- writer,
80
- has_message : false ,
102
+ shared : & mut self . shared ,
81
103
}
82
104
83
105
}
84
106
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
+
85
113
/// A special log type that is interpreted as a background task, on machine readable
86
114
/// outputs it acts as a regular log, but on human-readable outputs it will be
87
115
/// displayed at the end of the current line.
88
116
#[ inline]
89
117
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)
103
119
}
104
120
105
121
}
@@ -109,18 +125,21 @@ impl<'o> LogOutput<'o> {
109
125
pub struct Log < ' a , const BG : bool > {
110
126
/// Exclusive access to output.
111
127
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 ,
116
130
}
117
131
118
132
impl < const BG : bool > Log < ' _ , BG > {
119
133
134
+ // Reminder:
135
+ // \x1b[s save current cursor position
136
+ // \x1b[u restore saved cursor position
137
+ // \x1b[K clear the whole line
138
+
120
139
/// Append an argument for machine-readable output.
121
140
pub fn arg < D : Display > ( & mut self , arg : D ) -> & mut Self {
122
141
if let OutputMode :: TabSeparated { } = self . output . mode {
123
- write ! ( self . writer , "\t {arg}" ) . unwrap ( ) ;
142
+ write ! ( self . shared . line , "\t {arg}" ) . unwrap ( ) ;
124
143
}
125
144
self
126
145
}
@@ -133,12 +152,45 @@ impl<const BG: bool> Log<'_, BG> {
133
152
{
134
153
if let OutputMode :: TabSeparated { } = self . output . mode {
135
154
for arg in args {
136
- write ! ( self . writer , "\t {arg}" ) . unwrap ( ) ;
155
+ write ! ( self . shared . line , "\t {arg}" ) . unwrap ( ) ;
137
156
}
138
157
}
139
158
self
140
159
}
141
160
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
+
142
194
}
143
195
144
196
impl Log < ' _ , false > {
@@ -160,20 +212,14 @@ impl Log<'_, false> {
160
212
LogLevel :: Error => ( "FAILED" , "\x1b [31m" ) ,
161
213
} ;
162
214
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 ( ) ;
167
218
} 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 ( ) ;
169
220
}
170
221
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 ) ;
177
223
178
224
}
179
225
}
@@ -213,8 +259,12 @@ impl Log<'_, true> {
213
259
/// overwrite any background message currently written on the current log line.
214
260
pub fn message < D : Display > ( & mut self , message : D ) -> & mut Self {
215
261
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
+
218
268
}
219
269
self
220
270
}
@@ -229,17 +279,17 @@ impl<const BACKGROUND: bool> Drop for Log<'_, BACKGROUND> {
229
279
// Save the position of the cursor at the end of the line, this is used to
230
280
// easily rewrite the background task.
231
281
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.
236
285
} 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 ( ) ;
239
291
}
240
292
241
- self . writer . flush ( ) . unwrap ( ) ;
242
-
243
293
}
244
294
}
245
295
@@ -314,31 +364,6 @@ impl DownloadTracker {
314
364
315
365
}
316
366
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
-
342
367
/// Find the SI unit of a given number and return the number scaled down to that unit.
343
368
pub fn number_si_unit ( num : f32 ) -> ( f32 , char ) {
344
369
match num {
@@ -348,61 +373,3 @@ pub fn number_si_unit(num: f32) -> (f32, char) {
348
373
_ => ( num / 1_000_000_000.0 , 'G' ) ,
349
374
}
350
375
}
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