22// Use of this source code is governed by a BSD-style license that can be
33// found in the LICENSE file.
44
5- import 'dart:async' show runZoned;
5+ import 'dart:async' show Timer, runZoned;
66import 'dart:io' as io show
77 IOSink,
88 stderr,
@@ -29,7 +29,7 @@ import 'package:meta/meta.dart';
2929/// which can be inspected by unit tetss.
3030class Logger {
3131 /// Constructs a logger for use in the tool.
32- Logger () : _logger = log.Logger .detached ('et' ) {
32+ Logger () : _logger = log.Logger .detached ('et' ), _test = false {
3333 _logger.level = statusLevel;
3434 _logger.onRecord.listen (_handler);
3535 _setupIoSink (io.stderr);
@@ -38,7 +38,7 @@ class Logger {
3838
3939 /// A logger for tests.
4040 @visibleForTesting
41- Logger .test () : _logger = log.Logger .detached ('et' ) {
41+ Logger .test () : _logger = log.Logger .detached ('et' ), _test = true {
4242 _logger.level = statusLevel;
4343 _logger.onRecord.listen ((log.LogRecord r) => _testLogs.add (r));
4444 }
@@ -94,6 +94,9 @@ class Logger {
9494
9595 final log.Logger _logger;
9696 final List <log.LogRecord > _testLogs = < log.LogRecord > [];
97+ final bool _test;
98+
99+ Spinner ? _status;
97100
98101 /// Get the current logging level.
99102 log.Level get level => _logger.level;
@@ -104,43 +107,234 @@ class Logger {
104107 }
105108
106109 /// 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);
109117 }
110118
111119 /// 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);
114127 }
115128
116129 /// 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);
119137 }
120138
121139 /// 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);
124147 }
125148
126149 /// Writes a number of spaces to stdout equal to the width of the terminal
127150 /// and emits a carriage return.
128151 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' );
130179 return ;
131180 }
132181 final int width = io.stdout.terminalColumns;
182+ final String backspaces = '\b ' * width;
133183 final String spaces = ' ' * width;
134- _ioSinkWrite (io.stdout, '$spaces \r ' );
184+ _ioSinkWrite (io.stdout, '$backspaces $ spaces $ backspaces ' );
135185 }
136186
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 ();
139199 _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;
140239 }
141240
142241 /// In a [Logger] constructed by [Logger.test] , this list will contain all of
143242 /// the [LogRecord] s emitted by the test.
144243 @visibleForTesting
145244 List <log.LogRecord > get testLogs => _testLogs;
146245}
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