2
2
// Use of this source code is governed by a BSD-style license that can be
3
3
// found in the LICENSE file.
4
4
5
- import 'dart:async' show runZoned;
5
+ import 'dart:async' show Timer, runZoned;
6
6
import 'dart:io' as io show
7
7
IOSink,
8
8
stderr,
@@ -29,7 +29,7 @@ import 'package:meta/meta.dart';
29
29
/// which can be inspected by unit tetss.
30
30
class Logger {
31
31
/// Constructs a logger for use in the tool.
32
- Logger () : _logger = log.Logger .detached ('et' ) {
32
+ Logger () : _logger = log.Logger .detached ('et' ), _test = false {
33
33
_logger.level = statusLevel;
34
34
_logger.onRecord.listen (_handler);
35
35
_setupIoSink (io.stderr);
@@ -38,7 +38,7 @@ class Logger {
38
38
39
39
/// A logger for tests.
40
40
@visibleForTesting
41
- Logger .test () : _logger = log.Logger .detached ('et' ) {
41
+ Logger .test () : _logger = log.Logger .detached ('et' ), _test = true {
42
42
_logger.level = statusLevel;
43
43
_logger.onRecord.listen ((log.LogRecord r) => _testLogs.add (r));
44
44
}
@@ -94,6 +94,9 @@ class Logger {
94
94
95
95
final log.Logger _logger;
96
96
final List <log.LogRecord > _testLogs = < log.LogRecord > [];
97
+ final bool _test;
98
+
99
+ Spinner ? _status;
97
100
98
101
/// Get the current logging level.
99
102
log.Level get level => _logger.level;
@@ -104,43 +107,234 @@ class Logger {
104
107
}
105
108
106
109
/// Record a log message at level [Logger.error] .
107
- void error (Object ? message, {int indent = 0 , bool newline = true }) {
108
- _emitLog (errorLevel, message, indent, newline);
110
+ void error (
111
+ Object ? message, {
112
+ int indent = 0 ,
113
+ bool newline = true ,
114
+ bool fit = false ,
115
+ }) {
116
+ _emitLog (errorLevel, message, indent, newline, fit);
109
117
}
110
118
111
119
/// Record a log message at level [Logger.warning] .
112
- void warning (Object ? message, {int indent = 0 , bool newline = true }) {
113
- _emitLog (warningLevel, message, indent, newline);
120
+ void warning (
121
+ Object ? message, {
122
+ int indent = 0 ,
123
+ bool newline = true ,
124
+ bool fit = false ,
125
+ }) {
126
+ _emitLog (warningLevel, message, indent, newline, fit);
114
127
}
115
128
116
129
/// Record a log message at level [Logger.warning] .
117
- void status (Object ? message, {int indent = 0 , bool newline = true }) {
118
- _emitLog (statusLevel, message, indent, newline);
130
+ void status (
131
+ Object ? message, {
132
+ int indent = 0 ,
133
+ bool newline = true ,
134
+ bool fit = false ,
135
+ }) {
136
+ _emitLog (statusLevel, message, indent, newline, fit);
119
137
}
120
138
121
139
/// Record a log message at level [Logger.info] .
122
- void info (Object ? message, {int indent = 0 , bool newline = true }) {
123
- _emitLog (infoLevel, message, indent, newline);
140
+ void info (
141
+ Object ? message, {
142
+ int indent = 0 ,
143
+ bool newline = true ,
144
+ bool fit = false ,
145
+ }) {
146
+ _emitLog (infoLevel, message, indent, newline, fit);
124
147
}
125
148
126
149
/// Writes a number of spaces to stdout equal to the width of the terminal
127
150
/// and emits a carriage return.
128
151
void clearLine () {
129
- if (! io.stdout.hasTerminal) {
152
+ if (! io.stdout.hasTerminal || _test) {
153
+ return ;
154
+ }
155
+ _status? .pause ();
156
+ _emitClearLine ();
157
+ _status? .resume ();
158
+ }
159
+
160
+ /// Starts printing a progress spinner.
161
+ Spinner startSpinner ({
162
+ void Function ()? onFinish,
163
+ }) {
164
+ void finishCallback () {
165
+ onFinish? .call ();
166
+ _status = null ;
167
+ }
168
+ _status = io.stdout.hasTerminal && ! _test
169
+ ? FlutterSpinner (onFinish: finishCallback)
170
+ : Spinner (onFinish: finishCallback);
171
+ _status! .start ();
172
+ return _status! ;
173
+ }
174
+
175
+ static void _emitClearLine () {
176
+ if (io.stdout.supportsAnsiEscapes) {
177
+ // Go to start of the line and clear the line.
178
+ _ioSinkWrite (io.stdout, '\r\x 1B[K' );
130
179
return ;
131
180
}
132
181
final int width = io.stdout.terminalColumns;
182
+ final String backspaces = '\b ' * width;
133
183
final String spaces = ' ' * width;
134
- _ioSinkWrite (io.stdout, '$spaces \r ' );
184
+ _ioSinkWrite (io.stdout, '$backspaces $ spaces $ backspaces ' );
135
185
}
136
186
137
- void _emitLog (log.Level level, Object ? message, int indent, bool newline) {
138
- final String m = '${' ' * indent }$message ${newline ? '\n ' : '' }' ;
187
+ void _emitLog (
188
+ log.Level level,
189
+ Object ? message,
190
+ int indent,
191
+ bool newline,
192
+ bool fit,
193
+ ) {
194
+ String m = '${' ' * indent }$message ${newline ? '\n ' : '' }' ;
195
+ if (fit && io.stdout.hasTerminal) {
196
+ m = fitToWidth (m, io.stdout.terminalColumns);
197
+ }
198
+ _status? .pause ();
139
199
_logger.log (level, m);
200
+ _status? .resume ();
201
+ }
202
+
203
+ /// Shorten a string such that its length will be `w` by replacing
204
+ /// enough characters in the middle with '...'. Trailing whitespace will not
205
+ /// be preserved or counted against 'w', but if the input ends with a newline,
206
+ /// then the output will end with a newline that is not counted against 'w'.
207
+ /// That is, if the input string ends with a newline, the output string will
208
+ /// have length up to (w + 1) and end with a newline.
209
+ ///
210
+ /// If w <= 0, the result will be the empty string.
211
+ /// If w <= 3, the result will be a string containing w '.'s.
212
+ /// If there are a different number of non-'...' characters to the right and
213
+ /// left of '...' in the result, then the right will have one more than the
214
+ /// left.
215
+ @visibleForTesting
216
+ static String fitToWidth (String s, int w) {
217
+ // Preserve a trailing newline if needed.
218
+ final String maybeNewline = s.endsWith ('\n ' ) ? '\n ' : '' ;
219
+ if (w <= 0 ) {
220
+ return maybeNewline;
221
+ }
222
+ if (w <= 3 ) {
223
+ return '${'.' * w }$maybeNewline ' ;
224
+ }
225
+
226
+ // But remove trailing whitespace before removing the middle of the string.
227
+ s = s.trimRight ();
228
+ if (s.length <= w) {
229
+ return '$s $maybeNewline ' ;
230
+ }
231
+
232
+ // remove (s.length + 3 - w) characters from the middle of `s` and
233
+ // replace them with '...'.
234
+ final int diff = (s.length + 3 ) - w;
235
+ final int leftEnd = (s.length - diff) ~ / 2 ;
236
+ final int rightStart = (s.length + diff) ~ / 2 ;
237
+ s = s.replaceRange (leftEnd, rightStart, '...' );
238
+ return s + maybeNewline;
140
239
}
141
240
142
241
/// In a [Logger] constructed by [Logger.test] , this list will contain all of
143
242
/// the [LogRecord] s emitted by the test.
144
243
@visibleForTesting
145
244
List <log.LogRecord > get testLogs => _testLogs;
146
245
}
246
+
247
+
248
+ /// A base class for progress spinners, and a no-op implementation that prints
249
+ /// nothing.
250
+ class Spinner {
251
+ /// Creates a progress spinner. If supplied the `onDone` callback will be
252
+ /// called when `finish()` is called.
253
+ Spinner ({
254
+ this .onFinish,
255
+ });
256
+
257
+ /// The callback called when `finish()` is called.
258
+ final void Function ()? onFinish;
259
+
260
+ /// Starts the spinner animation.
261
+ void start () {}
262
+
263
+ /// Pauses the spinner animation. That is, this call causes printing to the
264
+ /// terminal to stop.
265
+ void pause () {}
266
+
267
+ /// Resumes the animation at the same from where `pause()` was called.
268
+ void resume () {}
269
+
270
+ /// Ends an animation, calling the `onFinish` callback if one was provided.
271
+ void finish () {
272
+ onFinish? .call ();
273
+ }
274
+ }
275
+
276
+ /// A [Spinner] implementation that prints an animated "Flutter" banner.
277
+ class FlutterSpinner extends Spinner {
278
+ // ignore: public_member_api_docs
279
+ FlutterSpinner ({
280
+ super .onFinish,
281
+ });
282
+
283
+ @visibleForTesting
284
+ /// The frames of the animation.
285
+ static const String frames = '⢸⡯⠭⠅⢸⣇⣀⡀⢸⣇⣸⡇⠈⢹⡏⠁⠈⢹⡏⠁⢸⣯⣭⡅⢸⡯⢕⡂⠀⠀' ;
286
+
287
+ static final List <String > _flutterAnimation = frames
288
+ .runes
289
+ .map <String >((int scalar) => String .fromCharCode (scalar))
290
+ .toList ();
291
+
292
+ Timer ? _timer;
293
+ int _ticks = 0 ;
294
+ int _lastAnimationFrameLength = 0 ;
295
+
296
+ @override
297
+ void start () {
298
+ _startSpinner ();
299
+ }
300
+
301
+ void _startSpinner () {
302
+ _timer = Timer .periodic (const Duration (milliseconds: 100 ), _callback);
303
+ _callback (_timer! );
304
+ }
305
+
306
+ void _callback (Timer timer) {
307
+ Logger ._ioSinkWrite (io.stdout, '\b ' * _lastAnimationFrameLength);
308
+ _ticks += 1 ;
309
+ final String newFrame = _currentAnimationFrame;
310
+ _lastAnimationFrameLength = newFrame.runes.length;
311
+ Logger ._ioSinkWrite (io.stdout, newFrame);
312
+ }
313
+
314
+ String get _currentAnimationFrame {
315
+ return _flutterAnimation[_ticks % _flutterAnimation.length];
316
+ }
317
+
318
+ @override
319
+ void pause () {
320
+ Logger ._emitClearLine ();
321
+ _lastAnimationFrameLength = 0 ;
322
+ _timer? .cancel ();
323
+ }
324
+
325
+ @override
326
+ void resume () {
327
+ _startSpinner ();
328
+ }
329
+
330
+ @override
331
+ void finish () {
332
+ _timer? .cancel ();
333
+ _timer = null ;
334
+ Logger ._emitClearLine ();
335
+ _lastAnimationFrameLength = 0 ;
336
+ if (onFinish != null ) {
337
+ onFinish !();
338
+ }
339
+ }
340
+ }
0 commit comments