logger.dart 22.3 KB
Newer Older
1 2 3 4
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'dart:async';
6

7
import 'package:meta/meta.dart';
8

9
import '../base/context.dart';
10
import 'io.dart';
11
import 'platform.dart';
12
import 'terminal.dart';
13
import 'utils.dart';
14

15
const int kDefaultStatusPadding = 59;
16 17 18 19 20 21
const Duration _kFastOperation = Duration(seconds: 2);
const Duration _kSlowOperation = Duration(minutes: 2);

/// The [TimeoutConfiguration] instance.
///
/// If not provided via injection, a default instance is provided.
22
TimeoutConfiguration get timeoutConfiguration => context.get<TimeoutConfiguration>() ?? const TimeoutConfiguration();
23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38

class TimeoutConfiguration {
  const TimeoutConfiguration();

  /// The expected time that various "slow" operations take, such as running
  /// the analyzer.
  ///
  /// Defaults to 2 minutes.
  Duration get slowOperation => _kSlowOperation;

  /// The expected time that various "fast" operations take, such as a hot
  /// reload.
  ///
  /// Defaults to 2 seconds.
  Duration get fastOperation => _kFastOperation;
}
39

40
typedef VoidCallback = void Function();
41

42
abstract class Logger {
Devon Carew's avatar
Devon Carew committed
43
  bool get isVerbose => false;
44

45 46
  bool quiet = false;

47
  bool get supportsColor => terminal.supportsColor;
48

49 50
  bool get hasTerminal => stdio.hasTerminal;

51
  /// Display an error `message` to the user. Commands should use this if they
52
  /// fail in some way.
53
  ///
54 55 56
  /// The `message` argument is printed to the stderr in red by default.
  ///
  /// The `stackTrace` argument is the stack trace that will be printed if
57
  /// supplied.
58 59 60 61
  ///
  /// The `emphasis` argument will cause the output message be printed in bold text.
  ///
  /// The `color` argument will print the message in the supplied color instead
62 63
  /// of the default of red. Colors will not be printed if the output terminal
  /// doesn't support them.
64 65
  ///
  /// The `indent` argument specifies the number of spaces to indent the overall
66 67
  /// message. If wrapping is enabled in [outputPreferences], then the wrapped
  /// lines will be indented as well.
68 69
  ///
  /// If `hangingIndent` is specified, then any wrapped lines will be indented
70 71
  /// by this much more than the first line, if wrapping is enabled in
  /// [outputPreferences].
72 73 74
  ///
  /// If `wrap` is specified, then it overrides the
  /// `outputPreferences.wrapText` setting.
75 76 77 78 79
  void printError(
    String message, {
    StackTrace stackTrace,
    bool emphasis,
    TerminalColor color,
80 81
    int indent,
    int hangingIndent,
82
    bool wrap,
83
  });
84 85 86

  /// Display normal output of the command. This should be used for things like
  /// progress messages, success messages, or just normal command output.
87
  ///
88 89 90
  /// The `message` argument is printed to the stderr in red by default.
  ///
  /// The `stackTrace` argument is the stack trace that will be printed if
91
  /// supplied.
92 93
  ///
  /// If the `emphasis` argument is true, it will cause the output message be
94
  /// printed in bold text. Defaults to false.
95 96
  ///
  /// The `color` argument will print the message in the supplied color instead
97 98
  /// of the default of red. Colors will not be printed if the output terminal
  /// doesn't support them.
99 100
  ///
  /// If `newline` is true, then a newline will be added after printing the
101
  /// status. Defaults to true.
102 103
  ///
  /// The `indent` argument specifies the number of spaces to indent the overall
104 105
  /// message. If wrapping is enabled in [outputPreferences], then the wrapped
  /// lines will be indented as well.
106 107
  ///
  /// If `hangingIndent` is specified, then any wrapped lines will be indented
108 109
  /// by this much more than the first line, if wrapping is enabled in
  /// [outputPreferences].
110 111 112
  ///
  /// If `wrap` is specified, then it overrides the
  /// `outputPreferences.wrapText` setting.
113
  void printStatus(
114 115 116 117 118
    String message, {
    bool emphasis,
    TerminalColor color,
    bool newline,
    int indent,
119
    int hangingIndent,
120
    bool wrap,
121
  });
122 123 124 125

  /// Use this for verbose tracing output. Users can turn this output on in order
  /// to help diagnose issues with the toolchain or with their setup.
  void printTrace(String message);
Devon Carew's avatar
Devon Carew committed
126

127
  /// Start an indeterminate progress display.
128
  ///
129 130 131 132 133
  /// The `message` argument is the message to display to the user.
  ///
  /// The `timeout` argument sets a duration after which an additional message
  /// may be shown saying that the operation is taking a long time. (Not all
  /// [Status] subclasses show such a message.) Set this to null if the
Chris Bracken's avatar
Chris Bracken committed
134
  /// operation can legitimately take an arbitrary amount of time (e.g. waiting
135
  /// for the user).
136
  ///
137 138 139 140 141
  /// The `progressId` argument provides an ID that can be used to identify
  /// this type of progress (e.g. `hot.reload`, `hot.restart`).
  ///
  /// The `progressIndicatorPadding` can optionally be used to specify spacing
  /// between the `message` and the progress indicator, if any.
142 143
  Status startProgress(
    String message, {
144
    @required Duration timeout,
145
    String progressId,
146 147
    bool multilineOutput = false,
    int progressIndicatorPadding = kDefaultStatusPadding,
148
  });
149

150
  /// Send an event to be emitted.
151 152
  ///
  /// Only surfaces a value in machine modes, Loggers may ignore this message in
153 154
  /// non-machine modes.
  void sendEvent(String name, [Map<String, dynamic> args]) { }
155 156
}

157
class StdoutLogger extends Logger {
158 159
  Status _status;

160
  @override
Devon Carew's avatar
Devon Carew committed
161
  bool get isVerbose => false;
162

163
  @override
164 165 166 167 168
  void printError(
    String message, {
    StackTrace stackTrace,
    bool emphasis,
    TerminalColor color,
169 170
    int indent,
    int hangingIndent,
171
    bool wrap,
172
  }) {
173
    _status?.pause();
174
    message ??= '';
175
    message = wrapText(message, indent: indent, hangingIndent: hangingIndent, shouldWrap: wrap);
176
    if (emphasis == true) {
177
      message = terminal.bolden(message);
178
    }
179
    message = terminal.color(message, color ?? TerminalColor.red);
Devon Carew's avatar
Devon Carew committed
180
    stderr.writeln(message);
181
    if (stackTrace != null) {
182
      stderr.writeln(stackTrace.toString());
183
    }
184
    _status?.resume();
185 186
  }

187
  @override
188
  void printStatus(
189 190 191 192 193
    String message, {
    bool emphasis,
    TerminalColor color,
    bool newline,
    int indent,
194
    int hangingIndent,
195
    bool wrap,
196
  }) {
197
    _status?.pause();
198
    message ??= '';
199
    message = wrapText(message, indent: indent, hangingIndent: hangingIndent, shouldWrap: wrap);
200
    if (emphasis == true) {
201
      message = terminal.bolden(message);
202 203
    }
    if (color != null) {
204
      message = terminal.color(message, color);
205 206
    }
    if (newline != false) {
207
      message = '$message\n';
208
    }
209
    writeToStdOut(message);
210
    _status?.resume();
211 212 213 214
  }

  @protected
  void writeToStdOut(String message) {
215
    stdout.write(message);
216
  }
217

218
  @override
219
  void printTrace(String message) { }
220

221
  @override
222 223
  Status startProgress(
    String message, {
224
    @required Duration timeout,
225
    String progressId,
226 227
    bool multilineOutput = false,
    int progressIndicatorPadding = kDefaultStatusPadding,
228
  }) {
229
    assert(progressIndicatorPadding != null);
Devon Carew's avatar
Devon Carew committed
230 231
    if (_status != null) {
      // Ignore nested progresses; return a no-op status object.
232 233 234 235
      return SilentStatus(
        timeout: timeout,
        onFinish: _clearStatus,
      )..start();
236 237
    }
    if (terminal.supportsColor) {
238
      _status = AnsiStatus(
239
        message: message,
240
        timeout: timeout,
241
        multilineOutput: multilineOutput,
242 243 244
        padding: progressIndicatorPadding,
        onFinish: _clearStatus,
      )..start();
Devon Carew's avatar
Devon Carew committed
245
    } else {
246 247
      _status = SummaryStatus(
        message: message,
248
        timeout: timeout,
249 250 251
        padding: progressIndicatorPadding,
        onFinish: _clearStatus,
      )..start();
252
    }
253
    return _status;
254
  }
255 256 257 258

  void _clearStatus() {
    _status = null;
  }
259 260

  @override
261
  void sendEvent(String name, [Map<String, dynamic> args]) { }
262 263
}

264 265 266
/// A [StdoutLogger] which replaces Unicode characters that cannot be printed to
/// the Windows console with alternative symbols.
///
267 268 269 270 271
/// By default, Windows uses either "Consolas" or "Lucida Console" as fonts to
/// render text in the console. Both fonts only have a limited character set.
/// Unicode characters, that are not available in either of the two default
/// fonts, should be replaced by this class with printable symbols. Otherwise,
/// they will show up as the unrepresentable character symbol '�'.
272 273 274
class WindowsStdoutLogger extends StdoutLogger {
  @override
  void writeToStdOut(String message) {
275
    // TODO(jcollins-g): wrong abstraction layer for this, move to [Stdio].
276 277
    stdout.write(message
        .replaceAll('✗', 'X')
278
        .replaceAll('✓', '√')
279 280 281 282
    );
  }
}

283
class BufferLogger extends Logger {
284
  @override
Devon Carew's avatar
Devon Carew committed
285 286
  bool get isVerbose => false;

287 288 289
  final StringBuffer _error = StringBuffer();
  final StringBuffer _status = StringBuffer();
  final StringBuffer _trace = StringBuffer();
290 291 292 293 294

  String get errorText => _error.toString();
  String get statusText => _status.toString();
  String get traceText => _trace.toString();

295 296 297
  @override
  bool get hasTerminal => false;

298
  @override
299 300 301 302 303
  void printError(
    String message, {
    StackTrace stackTrace,
    bool emphasis,
    TerminalColor color,
304 305
    int indent,
    int hangingIndent,
306
    bool wrap,
307
  }) {
308
    _error.writeln(terminal.color(
309
      wrapText(message, indent: indent, hangingIndent: hangingIndent, shouldWrap: wrap),
310 311
      color ?? TerminalColor.red,
    ));
312 313
  }

314
  @override
315
  void printStatus(
316 317 318 319 320
    String message, {
    bool emphasis,
    TerminalColor color,
    bool newline,
    int indent,
321
    int hangingIndent,
322
    bool wrap,
323
  }) {
324
    if (newline != false) {
325
      _status.writeln(wrapText(message, indent: indent, hangingIndent: hangingIndent, shouldWrap: wrap));
326
    } else {
327
      _status.write(wrapText(message, indent: indent, hangingIndent: hangingIndent, shouldWrap: wrap));
328
    }
329
  }
330 331

  @override
332
  void printTrace(String message) => _trace.writeln(message);
Devon Carew's avatar
Devon Carew committed
333

334
  @override
335 336
  Status startProgress(
    String message, {
337
    @required Duration timeout,
338
    String progressId,
339 340
    bool multilineOutput = false,
    int progressIndicatorPadding = kDefaultStatusPadding,
341
  }) {
342
    assert(progressIndicatorPadding != null);
343
    printStatus(message);
344
    return SilentStatus(timeout: timeout)..start();
345
  }
346 347 348 349 350 351 352

  /// Clears all buffers.
  void clear() {
    _error.clear();
    _status.clear();
    _trace.clear();
  }
353 354

  @override
355
  void sendEvent(String name, [Map<String, dynamic> args]) { }
Devon Carew's avatar
Devon Carew committed
356 357
}

358
class VerboseLogger extends Logger {
359
  VerboseLogger(this.parent) : assert(terminal != null) {
360
    _stopwatch.start();
361
  }
Devon Carew's avatar
Devon Carew committed
362

363 364
  final Logger parent;

365
  final Stopwatch _stopwatch = context.get<Stopwatch>() ?? Stopwatch();
366

367
  @override
Devon Carew's avatar
Devon Carew committed
368 369
  bool get isVerbose => true;

370
  @override
371 372 373 374 375
  void printError(
    String message, {
    StackTrace stackTrace,
    bool emphasis,
    TerminalColor color,
376 377
    int indent,
    int hangingIndent,
378
    bool wrap,
379
  }) {
380 381
    _emit(
      _LogType.error,
382
      wrapText(message, indent: indent, hangingIndent: hangingIndent, shouldWrap: wrap),
383 384
      stackTrace,
    );
Devon Carew's avatar
Devon Carew committed
385 386
  }

387
  @override
388
  void printStatus(
389 390 391 392 393
    String message, {
    bool emphasis,
    TerminalColor color,
    bool newline,
    int indent,
394
    int hangingIndent,
395
    bool wrap,
396
  }) {
397
    _emit(_LogType.status, wrapText(message, indent: indent, hangingIndent: hangingIndent, shouldWrap: wrap));
Devon Carew's avatar
Devon Carew committed
398 399
  }

400
  @override
Devon Carew's avatar
Devon Carew committed
401
  void printTrace(String message) {
402
    _emit(_LogType.trace, message);
Devon Carew's avatar
Devon Carew committed
403 404
  }

405
  @override
406 407
  Status startProgress(
    String message, {
408
    @required Duration timeout,
409
    String progressId,
410 411
    bool multilineOutput = false,
    int progressIndicatorPadding = kDefaultStatusPadding,
412
  }) {
413
    assert(progressIndicatorPadding != null);
414
    printStatus(message);
415 416 417 418 419
    final Stopwatch timer = Stopwatch()..start();
    return SilentStatus(
      timeout: timeout,
      onFinish: () {
        String time;
420
        if (timeout == null || timeout > timeoutConfiguration.fastOperation) {
421 422 423 424 425 426 427 428 429 430 431
          time = getElapsedAsSeconds(timer.elapsed);
        } else {
          time = getElapsedAsMilliseconds(timer.elapsed);
        }
        if (timeout != null && timer.elapsed > timeout) {
          printTrace('$message (completed in $time, longer than expected)');
        } else {
          printTrace('$message (completed in $time)');
        }
      },
    )..start();
432 433
  }

434
  void _emit(_LogType type, String message, [ StackTrace stackTrace ]) {
435
    if (message.trim().isEmpty) {
436
      return;
437
    }
Devon Carew's avatar
Devon Carew committed
438

439 440
    final int millis = _stopwatch.elapsedMilliseconds;
    _stopwatch.reset();
Devon Carew's avatar
Devon Carew committed
441

442
    String prefix;
443
    const int prefixWidth = 8;
444 445 446 447
    if (millis == 0) {
      prefix = ''.padLeft(prefixWidth);
    } else {
      prefix = '+$millis ms'.padLeft(prefixWidth);
448
      if (millis >= 100) {
449
        prefix = terminal.bolden(prefix);
450
      }
451 452
    }
    prefix = '[$prefix] ';
Devon Carew's avatar
Devon Carew committed
453

454 455
    final String indent = ''.padLeft(prefix.length);
    final String indentMessage = message.replaceAll('\n', '\n$indent');
Devon Carew's avatar
Devon Carew committed
456 457

    if (type == _LogType.error) {
458
      parent.printError(prefix + terminal.bolden(indentMessage));
459
      if (stackTrace != null) {
460
        parent.printError(indent + stackTrace.toString().replaceAll('\n', '\n$indent'));
461
      }
Devon Carew's avatar
Devon Carew committed
462
    } else if (type == _LogType.status) {
463
      parent.printStatus(prefix + terminal.bolden(indentMessage));
Devon Carew's avatar
Devon Carew committed
464
    } else {
465
      parent.printStatus(prefix + indentMessage);
Devon Carew's avatar
Devon Carew committed
466 467
    }
  }
468 469

  @override
470
  void sendEvent(String name, [Map<String, dynamic> args]) { }
Devon Carew's avatar
Devon Carew committed
471 472
}

473
enum _LogType { error, status, trace }
474

475 476
typedef SlowWarningCallback = String Function();

477 478 479
/// A [Status] class begins when start is called, and may produce progress
/// information asynchronously.
///
480 481 482 483
/// Some subclasses change output once [timeout] has expired, to indicate that
/// something is taking longer than expected.
///
/// The [SilentStatus] class never has any output.
484 485 486 487 488 489 490 491
///
/// The [AnsiSpinner] subclass shows a spinner, and replaces it with a single
/// space character when stopped or canceled.
///
/// The [AnsiStatus] subclass shows a spinner, and replaces it with timing
/// information when stopped. When canceled, the information isn't shown. In
/// either case, a newline is printed.
///
492 493 494
/// The [SummaryStatus] subclass shows only a static message (without an
/// indicator), then updates it when the operation ends.
///
495 496
/// Generally, consider `logger.startProgress` instead of directly creating
/// a [Status] or one of its subclasses.
497 498
abstract class Status {
  Status({ @required this.timeout, this.onFinish });
499

500
  /// A [SilentStatus] or an [AnsiSpinner] (depending on whether the
501
  /// terminal is fancy enough), already started.
502 503 504 505 506
  factory Status.withSpinner({
    @required Duration timeout,
    VoidCallback onFinish,
    SlowWarningCallback slowWarningCallback,
  }) {
507 508 509 510 511 512 513
    if (terminal.supportsColor) {
      return AnsiSpinner(
        timeout: timeout,
        onFinish: onFinish,
        slowWarningCallback: slowWarningCallback,
      )..start();
    }
514
    return SilentStatus(timeout: timeout, onFinish: onFinish)..start();
515 516
  }

517
  final Duration timeout;
518 519
  final VoidCallback onFinish;

520
  @protected
521
  final Stopwatch _stopwatch = context.get<Stopwatch>() ?? Stopwatch();
522 523

  @protected
524
  @visibleForTesting
525 526 527 528
  bool get seemsSlow => timeout != null && _stopwatch.elapsed > timeout;

  @protected
  String get elapsedTime {
529
    if (timeout == null || timeout > timeoutConfiguration.fastOperation) {
530
      return getElapsedAsSeconds(_stopwatch.elapsed);
531
    }
532 533
    return getElapsedAsMilliseconds(_stopwatch.elapsed);
  }
534

535
  /// Call to start spinning.
536
  void start() {
537 538
    assert(!_stopwatch.isRunning);
    _stopwatch.start();
539 540
  }

541
  /// Call to stop spinning after success.
542
  void stop() {
543
    finish();
544 545
  }

546
  /// Call to cancel the spinner after failure or cancellation.
547
  void cancel() {
548 549 550 551 552 553 554 555 556 557 558 559 560
    finish();
  }

  /// Call to clear the current line but not end the progress.
  void pause() { }

  /// Call to resume after a pause.
  void resume() { }

  @protected
  void finish() {
    assert(_stopwatch.isRunning);
    _stopwatch.stop();
561
    if (onFinish != null) {
562
      onFinish();
563
    }
564 565 566
  }
}

567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605
/// A [SilentStatus] shows nothing.
class SilentStatus extends Status {
  SilentStatus({
    @required Duration timeout,
    VoidCallback onFinish,
  }) : super(timeout: timeout, onFinish: onFinish);
}

/// Constructor writes [message] to [stdout].  On [cancel] or [stop], will call
/// [onFinish]. On [stop], will additionally print out summary information.
class SummaryStatus extends Status {
  SummaryStatus({
    this.message = '',
    @required Duration timeout,
    this.padding = kDefaultStatusPadding,
    VoidCallback onFinish,
  }) : assert(message != null),
       assert(padding != null),
       super(timeout: timeout, onFinish: onFinish);

  final String message;
  final int padding;

  bool _messageShowingOnCurrentLine = false;

  @override
  void start() {
    _printMessage();
    super.start();
  }

  void _printMessage() {
    assert(!_messageShowingOnCurrentLine);
    stdout.write('${message.padRight(padding)}     ');
    _messageShowingOnCurrentLine = true;
  }

  @override
  void stop() {
606
    if (!_messageShowingOnCurrentLine) {
607
      _printMessage();
608
    }
609 610 611 612 613 614 615 616
    super.stop();
    writeSummaryInformation();
    stdout.write('\n');
  }

  @override
  void cancel() {
    super.cancel();
617
    if (_messageShowingOnCurrentLine) {
618
      stdout.write('\n');
619
    }
620 621 622 623 624 625 626 627 628 629 630 631
  }

  /// Prints a (minimum) 8 character padded time.
  ///
  /// If [timeout] is less than or equal to [kFastOperation], the time is in
  /// seconds; otherwise, milliseconds. If the time is longer than [timeout],
  /// appends "(!)" to the time.
  ///
  /// Examples: `    0.5s`, `   150ms`, ` 1,600ms`, `    3.1s (!)`
  void writeSummaryInformation() {
    assert(_messageShowingOnCurrentLine);
    stdout.write(elapsedTime.padLeft(_kTimePadding));
632
    if (seemsSlow) {
633
      stdout.write(' (!)');
634
    }
635 636 637 638 639 640 641 642 643 644
  }

  @override
  void pause() {
    super.pause();
    stdout.write('\n');
    _messageShowingOnCurrentLine = false;
  }
}

645
/// An [AnsiSpinner] is a simple animation that does nothing but implement a
646 647 648 649
/// terminal spinner. When stopped or canceled, the animation erases itself.
///
/// If the timeout expires, a customizable warning is shown (but the spinner
/// continues otherwise unabated).
650
class AnsiSpinner extends Status {
651 652 653 654 655
  AnsiSpinner({
    @required Duration timeout,
    VoidCallback onFinish,
    this.slowWarningCallback,
  }) : super(timeout: timeout, onFinish: onFinish);
656

657 658 659
  final String _backspaceChar = '\b';
  final String _clearChar = ' ';

660 661
  bool timedOut = false;

662 663 664
  int ticks = 0;
  Timer timer;

665 666 667
  // Windows console font has a limited set of Unicode characters.
  List<String> get _animation => platform.isWindows
      ? <String>[r'-', r'\', r'|', r'/']
668
      : <String>['⣾', '⣽', '⣻', '⢿', '⡿', '⣟', '⣯', '⣷'];
669

670 671
  static const String _defaultSlowWarning = '(This is taking an unexpectedly long time.)';
  final SlowWarningCallback slowWarningCallback;
672

673 674 675 676
  String _slowWarning = '';

  String get _currentAnimationFrame => _animation[ticks % _animation.length];
  int get _currentLength => _currentAnimationFrame.length + _slowWarning.length;
677 678
  String get _backspace => _backspaceChar * (spinnerIndent + _currentLength);
  String get _clear => _clearChar *  (spinnerIndent + _currentLength);
679 680 681

  @protected
  int get spinnerIndent => 0;
682 683 684 685

  @override
  void start() {
    super.start();
686
    assert(timer == null);
687 688 689 690
    _startSpinner();
  }

  void _startSpinner() {
691
    stdout.write(_clear); // for _callback to backspace over
692
    timer = Timer.periodic(const Duration(milliseconds: 100), _callback);
693 694 695
    _callback(timer);
  }

696 697 698 699
  void _callback(Timer timer) {
    assert(this.timer == timer);
    assert(timer != null);
    assert(timer.isActive);
700
    stdout.write(_backspace);
701 702
    ticks += 1;
    if (seemsSlow) {
703 704 705 706
      if (!timedOut) {
        timedOut = true;
        stdout.write('$_clear\n');
      }
707
      if (slowWarningCallback != null) {
708
        _slowWarning = slowWarningCallback();
709
      } else {
710
        _slowWarning = _defaultSlowWarning;
711 712 713
      }
      stdout.write(_slowWarning);
    }
714
    stdout.write('${_clearChar * spinnerIndent}$_currentAnimationFrame');
715 716
  }

717
  @override
718 719
  void finish() {
    assert(timer != null);
720 721
    assert(timer.isActive);
    timer.cancel();
722 723 724 725 726 727
    timer = null;
    _clearSpinner();
    super.finish();
  }

  void _clearSpinner() {
728
    stdout.write('$_backspace$_clear$_backspace');
729 730
  }

731
  @override
732 733
  void pause() {
    assert(timer != null);
734
    assert(timer.isActive);
735
    _clearSpinner();
736
    timer.cancel();
737 738 739 740 741 742 743
  }

  @override
  void resume() {
    assert(timer != null);
    assert(!timer.isActive);
    _startSpinner();
744 745 746
  }
}

747 748 749 750 751 752 753 754
const int _kTimePadding = 8; // should fit "99,999ms"

/// Constructor writes [message] to [stdout] with padding, then starts an
/// indeterminate progress indicator animation (it's a subclass of
/// [AnsiSpinner]).
///
/// On [cancel] or [stop], will call [onFinish]. On [stop], will
/// additionally print out summary information.
755
class AnsiStatus extends AnsiSpinner {
756
  AnsiStatus({
757 758 759 760
    this.message = '',
    @required Duration timeout,
    this.multilineOutput = false,
    this.padding = kDefaultStatusPadding,
761
    VoidCallback onFinish,
762 763 764 765
  }) : assert(message != null),
       assert(multilineOutput != null),
       assert(padding != null),
       super(timeout: timeout, onFinish: onFinish);
766

767
  final String message;
768
  final bool multilineOutput;
769 770
  final int padding;

771 772
  static const String _margin = '     ';

773 774 775 776 777
  @override
  int get spinnerIndent => _kTimePadding - 1;

  int _totalMessageLength;

778 779
  @override
  void start() {
780
    _startStatus();
781
    super.start();
782
  }
783

784 785 786 787 788 789
  void _startStatus() {
    final String line = '${message.padRight(padding)}$_margin';
    _totalMessageLength = line.length;
    stdout.write(line);
  }

790
  @override
791
  void stop() {
792 793 794
    super.stop();
    writeSummaryInformation();
    stdout.write('\n');
795
  }
796

797
  @override
798 799 800 801 802
  void cancel() {
    super.cancel();
    stdout.write('\n');
  }

803 804
  /// Print summary information when a task is done.
  ///
805
  /// If [multilineOutput] is false, replaces the spinner with the summary message.
806
  ///
807
  /// If [multilineOutput] is true, then it prints the message again on a new
808
  /// line before writing the elapsed time.
809
  void writeSummaryInformation() {
810
    if (multilineOutput) {
811
      stdout.write('\n${'$message Done'.padRight(padding)}$_margin');
812
    }
813
    stdout.write(elapsedTime.padLeft(_kTimePadding));
814
    if (seemsSlow) {
815
      stdout.write(' (!)');
816
    }
817
  }
818

819
  void _clearStatus() {
820
    stdout.write('${_backspaceChar * _totalMessageLength}${_clearChar * _totalMessageLength}${_backspaceChar * _totalMessageLength}');
821 822 823
  }

  @override
824 825 826
  void pause() {
    super.pause();
    _clearStatus();
827 828 829
  }

  @override
830 831 832
  void resume() {
    _startStatus();
    super.resume();
833 834
  }
}