// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:async'; import 'dart:math'; import 'package:meta/meta.dart'; import '../convert.dart'; import 'common.dart'; import 'io.dart'; import 'terminal.dart' show OutputPreferences, Terminal, TerminalColor; import 'utils.dart'; const int kDefaultStatusPadding = 59; /// A factory for generating [Stopwatch] instances for [Status] instances. class StopwatchFactory { /// const constructor so that subclasses may be const. const StopwatchFactory(); /// Create a new [Stopwatch] instance. /// /// The optional [name] parameter is useful in tests when there are multiple /// instances being created. Stopwatch createStopwatch([String name = '']) => Stopwatch(); } typedef VoidCallback = void Function(); abstract class Logger { /// Whether or not this logger should print [printTrace] messages. bool get isVerbose => false; /// If true, silences the logger output. bool quiet = false; /// If true, this logger supports color output. bool get supportsColor; /// If true, this logger is connected to a terminal. bool get hasTerminal; /// If true, then [printError] has been called at least once for this logger /// since the last time it was set to false. bool hadErrorOutput = false; /// If true, then [printWarning] has been called at least once for this logger /// since the last time it was reset to false. bool hadWarningOutput = false; /// Causes [checkForFatalLogs] to call [throwToolExit] when it is called if /// [hadWarningOutput] is true. bool fatalWarnings = false; /// Returns the terminal attached to this logger. Terminal get terminal; OutputPreferences get _outputPreferences; /// Display an error `message` to the user. Commands should use this if they /// fail in some way. Errors are typically followed shortly by a call to /// [throwToolExit] to terminate the run. /// /// The `message` argument is printed to the stderr in [TerminalColor.red] by /// default. /// /// The `stackTrace` argument is the stack trace that will be printed if /// supplied. /// /// 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 /// of the default of red. Colors will not be printed if the output terminal /// doesn't support them. /// /// The `indent` argument specifies the number of spaces to indent the overall /// message. If wrapping is enabled in [outputPreferences], then the wrapped /// lines will be indented as well. /// /// If `hangingIndent` is specified, then any wrapped lines will be indented /// by this much more than the first line, if wrapping is enabled in /// [outputPreferences]. /// /// If `wrap` is specified, then it overrides the /// `outputPreferences.wrapText` setting. void printError( String message, { StackTrace? stackTrace, bool? emphasis, TerminalColor? color, int? indent, int? hangingIndent, bool? wrap, }); /// Display a warning `message` to the user. Commands should use this if they /// important information to convey to the user that is not fatal. /// /// The `message` argument is printed to the stderr in [TerminalColor.cyan] by /// default. /// /// 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 /// of the default of cyan. Colors will not be printed if the output terminal /// doesn't support them. /// /// The `indent` argument specifies the number of spaces to indent the overall /// message. If wrapping is enabled in [outputPreferences], then the wrapped /// lines will be indented as well. /// /// If `hangingIndent` is specified, then any wrapped lines will be indented /// by this much more than the first line, if wrapping is enabled in /// [outputPreferences]. /// /// If `wrap` is specified, then it overrides the /// `outputPreferences.wrapText` setting. void printWarning( String message, { bool? emphasis, TerminalColor? color, int? indent, int? hangingIndent, bool? wrap, }); /// Display normal output of the command. This should be used for things like /// progress messages, success messages, or just normal command output. /// /// The `message` argument is printed to the stdout. /// /// The `stackTrace` argument is the stack trace that will be printed if /// supplied. /// /// If the `emphasis` argument is true, it will cause the output message be /// printed in bold text. Defaults to false. /// /// The `color` argument will print the message in the supplied color instead /// of the default of red. Colors will not be printed if the output terminal /// doesn't support them. /// /// If `newline` is true, then a newline will be added after printing the /// status. Defaults to true. /// /// The `indent` argument specifies the number of spaces to indent the overall /// message. If wrapping is enabled in [outputPreferences], then the wrapped /// lines will be indented as well. /// /// If `hangingIndent` is specified, then any wrapped lines will be indented /// by this much more than the first line, if wrapping is enabled in /// [outputPreferences]. /// /// If `wrap` is specified, then it overrides the /// `outputPreferences.wrapText` setting. void printStatus( String message, { bool? emphasis, TerminalColor? color, bool? newline, int? indent, int? hangingIndent, bool? wrap, }); /// Display the [message] inside a box. /// /// For example, this is the generated output: /// /// ┌─ [title] ─┐ /// │ [message] │ /// └───────────┘ /// /// If a terminal is attached, the lines in [message] are automatically wrapped based on /// the available columns. /// /// Use this utility only to highlight a message in the logs. /// /// This is particularly useful when the message can be easily missed because of clutter /// generated by other commands invoked by the tool. /// /// One common use case is to provide actionable steps in a Flutter app when a Gradle /// error is printed. /// /// In the future, this output can be integrated with an IDE like VS Code to display a /// notification, and allow the user to trigger an action. e.g. run a migration. void printBox( String message, { String? title, }); /// 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); /// Start an indeterminate progress display. /// /// The `message` argument is the message to display to the user. /// /// 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 the width /// of the space into which the `message` is placed before the progress /// indicator, if any. It is ignored if the message is longer. Status startProgress( String message, { String? progressId, int progressIndicatorPadding = kDefaultStatusPadding, }); /// A [SilentStatus] or an [AnonymousSpinnerStatus] (depending on whether the /// terminal is fancy enough), already started. Status startSpinner({ VoidCallback? onFinish, Duration? timeout, SlowWarningCallback? slowWarningCallback, }); /// Send an event to be emitted. /// /// Only surfaces a value in machine modes, Loggers may ignore this message in /// non-machine modes. void sendEvent(String name, [Map<String, dynamic>? args]) { } /// Clears all output. void clear(); /// If [fatalWarnings] is set, causes the logger to check if /// [hadWarningOutput] is true, and then to call [throwToolExit] if so. /// /// The [fatalWarnings] flag can be set from the command line with the /// "--fatal-warnings" option on commands that support it. void checkForFatalLogs() { if (fatalWarnings && (hadWarningOutput || hadErrorOutput)) { throwToolExit('Logger received ${hadErrorOutput ? 'error' : 'warning'} output ' 'during the run, and "--fatal-warnings" is enabled.'); } } } /// A [Logger] that forwards all methods to another logger. /// /// Classes can derive from this to add functionality to an existing [Logger]. class DelegatingLogger implements Logger { @visibleForTesting @protected DelegatingLogger(this._delegate); final Logger _delegate; @override bool get quiet => _delegate.quiet; @override set quiet(bool value) => _delegate.quiet = value; @override bool get hasTerminal => _delegate.hasTerminal; @override Terminal get terminal => _delegate.terminal; @override OutputPreferences get _outputPreferences => _delegate._outputPreferences; @override bool get isVerbose => _delegate.isVerbose; @override bool get hadErrorOutput => _delegate.hadErrorOutput; @override set hadErrorOutput(bool value) => _delegate.hadErrorOutput = value; @override bool get hadWarningOutput => _delegate.hadWarningOutput; @override set hadWarningOutput(bool value) => _delegate.hadWarningOutput = value; @override bool get fatalWarnings => _delegate.fatalWarnings; @override set fatalWarnings(bool value) => _delegate.fatalWarnings = value; @override void printError(String message, { StackTrace? stackTrace, bool? emphasis, TerminalColor? color, int? indent, int? hangingIndent, bool? wrap, }) { _delegate.printError( message, stackTrace: stackTrace, emphasis: emphasis, color: color, indent: indent, hangingIndent: hangingIndent, wrap: wrap, ); } @override void printWarning(String message, { bool? emphasis, TerminalColor? color, int? indent, int? hangingIndent, bool? wrap, }) { _delegate.printWarning( message, emphasis: emphasis, color: color, indent: indent, hangingIndent: hangingIndent, wrap: wrap, ); } @override void printStatus(String message, { bool? emphasis, TerminalColor? color, bool? newline, int? indent, int? hangingIndent, bool? wrap, }) { _delegate.printStatus(message, emphasis: emphasis, color: color, newline: newline, indent: indent, hangingIndent: hangingIndent, wrap: wrap, ); } @override void printBox(String message, { String? title, }) { _delegate.printBox(message, title: title); } @override void printTrace(String message) { _delegate.printTrace(message); } @override void sendEvent(String name, [Map<String, dynamic>? args]) { _delegate.sendEvent(name, args); } @override Status startProgress(String message, { String? progressId, int progressIndicatorPadding = kDefaultStatusPadding, }) { return _delegate.startProgress(message, progressId: progressId, progressIndicatorPadding: progressIndicatorPadding, ); } @override Status startSpinner({ VoidCallback? onFinish, Duration? timeout, SlowWarningCallback? slowWarningCallback, }) { return _delegate.startSpinner( onFinish: onFinish, timeout: timeout, slowWarningCallback: slowWarningCallback, ); } @override bool get supportsColor => _delegate.supportsColor; @override void clear() => _delegate.clear(); @override void checkForFatalLogs() => _delegate.checkForFatalLogs(); } /// If [logger] is a [DelegatingLogger], walks the delegate chain and returns /// the first delegate with the matching type. /// /// Throws a [StateError] if no matching delegate is found. @override T asLogger<T extends Logger>(Logger logger) { final Logger original = logger; while (true) { if (logger is T) { return logger; } else if (logger is DelegatingLogger) { logger = logger._delegate; } else { throw StateError('$original has no ancestor delegate of type $T'); } } } class StdoutLogger extends Logger { StdoutLogger({ required this.terminal, required Stdio stdio, required OutputPreferences outputPreferences, StopwatchFactory stopwatchFactory = const StopwatchFactory(), }) : _stdio = stdio, _outputPreferences = outputPreferences, _stopwatchFactory = stopwatchFactory; @override final Terminal terminal; @override final OutputPreferences _outputPreferences; final Stdio _stdio; final StopwatchFactory _stopwatchFactory; Status? _status; @override bool get isVerbose => false; @override bool get supportsColor => terminal.supportsColor; @override bool get hasTerminal => _stdio.stdinHasTerminal; @override void printError( String message, { StackTrace? stackTrace, bool? emphasis, TerminalColor? color, int? indent, int? hangingIndent, bool? wrap, }) { hadErrorOutput = true; _status?.pause(); message = wrapText(message, indent: indent, hangingIndent: hangingIndent, shouldWrap: wrap ?? _outputPreferences.wrapText, columnWidth: _outputPreferences.wrapColumn, ); if (emphasis ?? false) { message = terminal.bolden(message); } message = terminal.color(message, color ?? TerminalColor.red); writeToStdErr('$message\n'); if (stackTrace != null) { writeToStdErr('$stackTrace\n'); } _status?.resume(); } @override void printWarning( String message, { bool? emphasis, TerminalColor? color, int? indent, int? hangingIndent, bool? wrap, }) { hadWarningOutput = true; _status?.pause(); message = wrapText(message, indent: indent, hangingIndent: hangingIndent, shouldWrap: wrap ?? _outputPreferences.wrapText, columnWidth: _outputPreferences.wrapColumn, ); if (emphasis ?? false) { message = terminal.bolden(message); } message = terminal.color(message, color ?? TerminalColor.cyan); writeToStdErr('$message\n'); _status?.resume(); } @override void printStatus( String message, { bool? emphasis, TerminalColor? color, bool? newline, int? indent, int? hangingIndent, bool? wrap, }) { _status?.pause(); message = wrapText(message, indent: indent, hangingIndent: hangingIndent, shouldWrap: wrap ?? _outputPreferences.wrapText, columnWidth: _outputPreferences.wrapColumn, ); if (emphasis ?? false) { message = terminal.bolden(message); } if (color != null) { message = terminal.color(message, color); } if (newline ?? true) { message = '$message\n'; } writeToStdOut(message); _status?.resume(); } @override void printBox(String message, { String? title, }) { _status?.pause(); _generateBox( title: title, message: message, wrapColumn: _outputPreferences.wrapColumn, terminal: terminal, write: writeToStdOut, ); _status?.resume(); } @protected void writeToStdOut(String message) => _stdio.stdoutWrite(message); @protected void writeToStdErr(String message) => _stdio.stderrWrite(message); @override void printTrace(String message) { } @override Status startProgress( String message, { String? progressId, int progressIndicatorPadding = kDefaultStatusPadding, }) { if (_status != null) { // Ignore nested progresses; return a no-op status object. return SilentStatus( stopwatch: _stopwatchFactory.createStopwatch(), )..start(); } if (supportsColor) { _status = SpinnerStatus( message: message, padding: progressIndicatorPadding, onFinish: _clearStatus, stdio: _stdio, stopwatch: _stopwatchFactory.createStopwatch(), terminal: terminal, )..start(); } else { _status = SummaryStatus( message: message, padding: progressIndicatorPadding, onFinish: _clearStatus, stdio: _stdio, stopwatch: _stopwatchFactory.createStopwatch(), )..start(); } return _status!; } @override Status startSpinner({ VoidCallback? onFinish, Duration? timeout, SlowWarningCallback? slowWarningCallback, }) { if (_status != null || !supportsColor) { return SilentStatus( onFinish: onFinish, stopwatch: _stopwatchFactory.createStopwatch(), )..start(); } _status = AnonymousSpinnerStatus( onFinish: () { if (onFinish != null) { onFinish(); } _clearStatus(); }, stdio: _stdio, stopwatch: _stopwatchFactory.createStopwatch(), terminal: terminal, timeout: timeout, slowWarningCallback: slowWarningCallback, )..start(); return _status!; } void _clearStatus() { _status = null; } @override void sendEvent(String name, [Map<String, dynamic>? args]) { } @override void clear() { _status?.pause(); writeToStdOut('${terminal.clearScreen()}\n'); _status?.resume(); } } typedef _Writter = void Function(String message); /// Wraps the message in a box, and writes the bytes by calling [write]. /// /// Example output: /// /// ┌─ [title] ─┐ /// │ [message] │ /// └───────────┘ /// /// When [title] is provided, the box will have a title above it. /// /// The box width never exceeds [wrapColumn]. /// /// If [wrapColumn] is not provided, the default value is 100. void _generateBox({ required String message, required int wrapColumn, required _Writter write, required Terminal terminal, String? title, }) { const int kPaddingLeftRight = 1; const int kEdges = 2; final int maxTextWidthPerLine = wrapColumn - kEdges - kPaddingLeftRight * 2; final List<String> lines = wrapText(message, shouldWrap: true, columnWidth: maxTextWidthPerLine).split('\n'); final List<int> lineWidth = lines.map((String line) => _getColumnSize(line)).toList(); final int maxColumnSize = lineWidth.reduce((int currLen, int maxLen) => max(currLen, maxLen)); final int textWidth = min(maxColumnSize, maxTextWidthPerLine); final int textWithPaddingWidth = textWidth + kPaddingLeftRight * 2; write('\n'); // Write `┌─ [title] ─┐`. write('┌'); write('─'); if (title == null) { write('─' * (textWithPaddingWidth - 1)); } else { write(' ${terminal.bolden(title)} '); write('─' * (textWithPaddingWidth - title.length - 3)); } write('┐'); write('\n'); // Write `│ [message] │`. for (int lineIdx = 0; lineIdx < lines.length; lineIdx++) { write('│'); write(' ' * kPaddingLeftRight); write(lines[lineIdx]); final int remainingSpacesToEnd = textWidth - lineWidth[lineIdx]; write(' ' * (remainingSpacesToEnd + kPaddingLeftRight)); write('│'); write('\n'); } // Write `└───────────┘`. write('└'); write('─' * textWithPaddingWidth); write('┘'); write('\n'); } final RegExp _ansiEscapePattern = RegExp('\x1B\\[[\x30-\x3F]*[\x20-\x2F]*[\x40-\x7E]'); int _getColumnSize(String line) { // Remove ANSI escape characters from the string. return line.replaceAll(_ansiEscapePattern, '').length; } /// A [StdoutLogger] which replaces Unicode characters that cannot be printed to /// the Windows console with alternative symbols. /// /// 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 '�'. class WindowsStdoutLogger extends StdoutLogger { WindowsStdoutLogger({ required super.terminal, required super.stdio, required super.outputPreferences, super.stopwatchFactory, }); @override void writeToStdOut(String message) { final String windowsMessage = terminal.supportsEmoji ? message : message.replaceAll('🔥', '') .replaceAll('🖼️', '') .replaceAll('✗', 'X') .replaceAll('✓', '√') .replaceAll('🔨', '') .replaceAll('💪', '') .replaceAll('⚠️', '!') .replaceAll('✏️', ''); _stdio.stdoutWrite(windowsMessage); } } class BufferLogger extends Logger { BufferLogger({ required this.terminal, required OutputPreferences outputPreferences, StopwatchFactory stopwatchFactory = const StopwatchFactory(), bool verbose = false, }) : _outputPreferences = outputPreferences, _stopwatchFactory = stopwatchFactory, _verbose = verbose; /// Create a [BufferLogger] with test preferences. BufferLogger.test({ Terminal? terminal, OutputPreferences? outputPreferences, bool verbose = false, }) : terminal = terminal ?? Terminal.test(), _outputPreferences = outputPreferences ?? OutputPreferences.test(), _stopwatchFactory = const StopwatchFactory(), _verbose = verbose; @override final OutputPreferences _outputPreferences; @override final Terminal terminal; final StopwatchFactory _stopwatchFactory; final bool _verbose; @override bool get isVerbose => _verbose; @override bool get supportsColor => terminal.supportsColor; final StringBuffer _error = StringBuffer(); final StringBuffer _warning = StringBuffer(); final StringBuffer _status = StringBuffer(); final StringBuffer _trace = StringBuffer(); final StringBuffer _events = StringBuffer(); String get errorText => _error.toString(); String get warningText => _warning.toString(); String get statusText => _status.toString(); String get traceText => _trace.toString(); String get eventText => _events.toString(); @override bool get hasTerminal => false; @override void printError( String message, { StackTrace? stackTrace, bool? emphasis, TerminalColor? color, int? indent, int? hangingIndent, bool? wrap, }) { hadErrorOutput = true; final StringBuffer errorMessage = StringBuffer(); errorMessage.write(message); if (stackTrace != null) { errorMessage.writeln(); errorMessage.write(stackTrace); } _error.writeln(terminal.color( wrapText(errorMessage.toString(), indent: indent, hangingIndent: hangingIndent, shouldWrap: wrap ?? _outputPreferences.wrapText, columnWidth: _outputPreferences.wrapColumn, ), color ?? TerminalColor.red, )); } @override void printWarning( String message, { bool? emphasis, TerminalColor? color, int? indent, int? hangingIndent, bool? wrap, }) { hadWarningOutput = true; _warning.writeln(terminal.color( wrapText(message, indent: indent, hangingIndent: hangingIndent, shouldWrap: wrap ?? _outputPreferences.wrapText, columnWidth: _outputPreferences.wrapColumn, ), color ?? TerminalColor.cyan, )); } @override void printStatus( String message, { bool? emphasis, TerminalColor? color, bool? newline, int? indent, int? hangingIndent, bool? wrap, }) { if (newline ?? true) { _status.writeln(wrapText(message, indent: indent, hangingIndent: hangingIndent, shouldWrap: wrap ?? _outputPreferences.wrapText, columnWidth: _outputPreferences.wrapColumn, )); } else { _status.write(wrapText(message, indent: indent, hangingIndent: hangingIndent, shouldWrap: wrap ?? _outputPreferences.wrapText, columnWidth: _outputPreferences.wrapColumn, )); } } @override void printBox(String message, { String? title, }) { _generateBox( title: title, message: message, wrapColumn: _outputPreferences.wrapColumn, terminal: terminal, write: _status.write, ); } @override void printTrace(String message) => _trace.writeln(message); @override Status startProgress( String message, { String? progressId, int progressIndicatorPadding = kDefaultStatusPadding, }) { assert(progressIndicatorPadding != null); printStatus(message); return SilentStatus( stopwatch: _stopwatchFactory.createStopwatch(), )..start(); } @override Status startSpinner({ VoidCallback? onFinish, Duration? timeout, SlowWarningCallback? slowWarningCallback, }) { return SilentStatus( stopwatch: _stopwatchFactory.createStopwatch(), onFinish: onFinish, )..start(); } @override void clear() { _error.clear(); _status.clear(); _trace.clear(); _events.clear(); } @override void sendEvent(String name, [Map<String, dynamic>? args]) { _events.write(json.encode(<String, Object?>{ 'name': name, 'args': args, })); } } class VerboseLogger extends DelegatingLogger { VerboseLogger(super.parent, { StopwatchFactory stopwatchFactory = const StopwatchFactory() }) : _stopwatch = stopwatchFactory.createStopwatch(), _stopwatchFactory = stopwatchFactory { _stopwatch.start(); } final Stopwatch _stopwatch; final StopwatchFactory _stopwatchFactory; @override bool get isVerbose => true; @override void printError( String message, { StackTrace? stackTrace, bool? emphasis, TerminalColor? color, int? indent, int? hangingIndent, bool? wrap, }) { hadErrorOutput = true; _emit( _LogType.error, wrapText(message, indent: indent, hangingIndent: hangingIndent, shouldWrap: wrap ?? _outputPreferences.wrapText, columnWidth: _outputPreferences.wrapColumn, ), stackTrace, ); } @override void printWarning( String message, { StackTrace? stackTrace, bool? emphasis, TerminalColor? color, int? indent, int? hangingIndent, bool? wrap, }) { hadWarningOutput = true; _emit( _LogType.warning, wrapText(message, indent: indent, hangingIndent: hangingIndent, shouldWrap: wrap ?? _outputPreferences.wrapText, columnWidth: _outputPreferences.wrapColumn, ), stackTrace, ); } @override void printStatus( String message, { bool? emphasis, TerminalColor? color, bool? newline, int? indent, int? hangingIndent, bool? wrap, }) { _emit(_LogType.status, wrapText(message, indent: indent, hangingIndent: hangingIndent, shouldWrap: wrap ?? _outputPreferences.wrapText, columnWidth: _outputPreferences.wrapColumn, )); } @override void printBox(String message, { String? title, }) { String composedMessage = ''; _generateBox( title: title, message: message, wrapColumn: _outputPreferences.wrapColumn, terminal: terminal, write: (String line) { composedMessage += line; }, ); _emit(_LogType.status, composedMessage); } @override void printTrace(String message) { _emit(_LogType.trace, message); } @override Status startProgress( String message, { String? progressId, int progressIndicatorPadding = kDefaultStatusPadding, }) { assert(progressIndicatorPadding != null); printStatus(message); final Stopwatch timer = _stopwatchFactory.createStopwatch()..start(); return SilentStatus( // This is intentionally a different stopwatch than above. stopwatch: _stopwatchFactory.createStopwatch(), onFinish: () { String time; if (timer.elapsed.inSeconds > 2) { time = getElapsedAsSeconds(timer.elapsed); } else { time = getElapsedAsMilliseconds(timer.elapsed); } printTrace('$message (completed in $time)'); }, )..start(); } void _emit(_LogType type, String message, [ StackTrace? stackTrace ]) { if (message.trim().isEmpty) { return; } final int millis = _stopwatch.elapsedMilliseconds; _stopwatch.reset(); String prefix; const int prefixWidth = 8; if (millis == 0) { prefix = ''.padLeft(prefixWidth); } else { prefix = '+$millis ms'.padLeft(prefixWidth); if (millis >= 100) { prefix = terminal.bolden(prefix); } } prefix = '[$prefix] '; final String indent = ''.padLeft(prefix.length); final String indentMessage = message.replaceAll('\n', '\n$indent'); switch (type) { case _LogType.error: super.printError(prefix + terminal.bolden(indentMessage)); if (stackTrace != null) { super.printError(indent + stackTrace.toString().replaceAll('\n', '\n$indent')); } break; case _LogType.warning: super.printWarning(prefix + terminal.bolden(indentMessage)); break; case _LogType.status: super.printStatus(prefix + terminal.bolden(indentMessage)); break; case _LogType.trace: // This seems wrong, since there is a 'printTrace' to call on the // superclass, but it's actually the entire point of this logger: to // make things more verbose than they normally would be. super.printStatus(prefix + indentMessage); break; } } @override void sendEvent(String name, [Map<String, dynamic>? args]) { } } class PrefixedErrorLogger extends DelegatingLogger { PrefixedErrorLogger(super.parent); @override void printError( String message, { StackTrace? stackTrace, bool? emphasis, TerminalColor? color, int? indent, int? hangingIndent, bool? wrap, }) { hadErrorOutput = true; if (message.trim().isNotEmpty == true) { message = 'ERROR: $message'; } super.printError( message, stackTrace: stackTrace, emphasis: emphasis, color: color, indent: indent, hangingIndent: hangingIndent, wrap: wrap, ); } } enum _LogType { error, warning, status, trace } typedef SlowWarningCallback = String Function(); /// A [Status] class begins when start is called, and may produce progress /// information asynchronously. /// /// The [SilentStatus] class never has any output. /// /// The [SpinnerStatus] subclass shows a message with a spinner, and replaces it /// with timing information when stopped. When canceled, the information isn't /// shown. In either case, a newline is printed. /// /// The [AnonymousSpinnerStatus] subclass just shows a spinner. /// /// The [SummaryStatus] subclass shows only a static message (without an /// indicator), then updates it when the operation ends. /// /// Generally, consider `logger.startProgress` instead of directly creating /// a [Status] or one of its subclasses. abstract class Status { Status({ this.onFinish, required Stopwatch stopwatch, this.timeout, }) : _stopwatch = stopwatch; final VoidCallback? onFinish; final Duration? timeout; @protected final Stopwatch _stopwatch; @protected String get elapsedTime { if (_stopwatch.elapsed.inSeconds > 2) { return getElapsedAsSeconds(_stopwatch.elapsed); } return getElapsedAsMilliseconds(_stopwatch.elapsed); } @visibleForTesting bool get seemsSlow => timeout != null && _stopwatch.elapsed > timeout!; /// Call to start spinning. void start() { assert(!_stopwatch.isRunning); _stopwatch.start(); } /// Call to stop spinning after success. void stop() { finish(); } /// Call to cancel the spinner after failure or cancellation. void cancel() { 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(); onFinish?.call(); } } /// A [Status] that shows nothing. class SilentStatus extends Status { SilentStatus({ required super.stopwatch, super.onFinish, }); @override void finish() { onFinish?.call(); } } const int _kTimePadding = 8; // should fit "99,999ms" /// 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 super.stopwatch, this.padding = kDefaultStatusPadding, super.onFinish, required Stdio stdio, }) : _stdio = stdio; final String message; final int padding; final Stdio _stdio; bool _messageShowingOnCurrentLine = false; @override void start() { _printMessage(); super.start(); } void _writeToStdOut(String message) => _stdio.stdoutWrite(message); void _printMessage() { assert(!_messageShowingOnCurrentLine); _writeToStdOut('${message.padRight(padding)} '); _messageShowingOnCurrentLine = true; } @override void stop() { if (!_messageShowingOnCurrentLine) { _printMessage(); } super.stop(); assert(_messageShowingOnCurrentLine); _writeToStdOut(elapsedTime.padLeft(_kTimePadding)); _writeToStdOut('\n'); } @override void cancel() { super.cancel(); if (_messageShowingOnCurrentLine) { _writeToStdOut('\n'); } } @override void pause() { super.pause(); if (_messageShowingOnCurrentLine) { _writeToStdOut('\n'); _messageShowingOnCurrentLine = false; } } } /// A kind of animated [Status] that has no message. /// /// Call [pause] before outputting any text while this is running. class AnonymousSpinnerStatus extends Status { AnonymousSpinnerStatus({ super.onFinish, required super.stopwatch, required Stdio stdio, required Terminal terminal, this.slowWarningCallback, super.timeout, }) : _stdio = stdio, _terminal = terminal, _animation = _selectAnimation(terminal); final Stdio _stdio; final Terminal _terminal; String _slowWarning = ''; final SlowWarningCallback? slowWarningCallback; static const String _backspaceChar = '\b'; static const String _clearChar = ' '; static const List<String> _emojiAnimations = <String>[ '⣾⣽⣻⢿⡿⣟⣯⣷', // counter-clockwise '⣾⣷⣯⣟⡿⢿⣻⣽', // clockwise '⣾⣷⣯⣟⡿⢿⣻⣽⣷⣾⣽⣻⢿⡿⣟⣯⣷', // bouncing clockwise and counter-clockwise '⣾⣷⣯⣽⣻⣟⡿⢿⣻⣟⣯⣽', // snaking '⣾⣽⣻⢿⣿⣷⣯⣟⡿⣿', // alternating rain '⣀⣠⣤⣦⣶⣾⣿⡿⠿⠻⠛⠋⠉⠙⠛⠟⠿⢿⣿⣷⣶⣴⣤⣄', // crawl up and down, large '⠙⠚⠖⠦⢤⣠⣄⡤⠴⠲⠓⠋', // crawl up and down, small '⣀⡠⠤⠔⠒⠊⠉⠑⠒⠢⠤⢄', // crawl up and down, tiny '⡀⣄⣦⢷⠻⠙⠈⠀⠁⠋⠟⡾⣴⣠⢀⠀', // slide up and down '⠙⠸⢰⣠⣄⡆⠇⠋', // clockwise line '⠁⠈⠐⠠⢀⡀⠄⠂', // clockwise dot '⢇⢣⢱⡸⡜⡎', // vertical wobble up '⡇⡎⡜⡸⢸⢱⢣⢇', // vertical wobble down '⡀⣀⣐⣒⣖⣶⣾⣿⢿⠿⠯⠭⠩⠉⠁⠀', // swirl '⠁⠐⠄⢀⢈⢂⢠⣀⣁⣐⣄⣌⣆⣤⣥⣴⣼⣶⣷⣿⣾⣶⣦⣤⣠⣀⡀⠀⠀', // snowing and melting '⠁⠋⠞⡴⣠⢀⠀⠈⠙⠻⢷⣦⣄⡀⠀⠉⠛⠲⢤⢀⠀', // falling water '⠄⡢⢑⠈⠀⢀⣠⣤⡶⠞⠋⠁⠀⠈⠙⠳⣆⡀⠀⠆⡷⣹⢈⠀⠐⠪⢅⡀⠀', // fireworks '⠐⢐⢒⣒⣲⣶⣷⣿⡿⡷⡧⠧⠇⠃⠁⠀⡀⡠⡡⡱⣱⣳⣷⣿⢿⢯⢧⠧⠣⠃⠂⠀⠈⠨⠸⠺⡺⡾⡿⣿⡿⡷⡗⡇⡅⡄⠄⠀⡀⡐⣐⣒⣓⣳⣻⣿⣾⣼⡼⡸⡘⡈⠈⠀', // fade '⢸⡯⠭⠅⢸⣇⣀⡀⢸⣇⣸⡇⠈⢹⡏⠁⠈⢹⡏⠁⢸⣯⣭⡅⢸⡯⢕⡂⠀⠀', // text crawl ]; static const List<String> _asciiAnimations = <String>[ r'-\|/', ]; static List<String> _selectAnimation(Terminal terminal) { final List<String> animations = terminal.supportsEmoji ? _emojiAnimations : _asciiAnimations; return animations[terminal.preferredStyle % animations.length] .runes .map<String>((int scalar) => String.fromCharCode(scalar)) .toList(); } final List<String> _animation; Timer? timer; int ticks = 0; int _lastAnimationFrameLength = 0; bool timedOut = false; String get _currentAnimationFrame => _animation[ticks % _animation.length]; int get _currentLineLength => _lastAnimationFrameLength + _slowWarning.length; void _writeToStdOut(String message) => _stdio.stdoutWrite(message); void _clear(int length) { _writeToStdOut( '${_backspaceChar * length}' '${_clearChar * length}' '${_backspaceChar * length}' ); } @override void start() { super.start(); assert(timer == null); _startSpinner(); } void _startSpinner() { timer = Timer.periodic(const Duration(milliseconds: 100), _callback); _callback(timer!); } void _callback(Timer timer) { assert(this.timer == timer); assert(timer != null); assert(timer.isActive); _writeToStdOut(_backspaceChar * _lastAnimationFrameLength); ticks += 1; if (seemsSlow) { if (!timedOut) { timedOut = true; _clear(_currentLineLength); } if (_slowWarning == '' && slowWarningCallback != null) { _slowWarning = slowWarningCallback!(); _writeToStdOut(_slowWarning); } } final String newFrame = _currentAnimationFrame; _lastAnimationFrameLength = newFrame.runes.length; _writeToStdOut(newFrame); } @override void pause() { assert(timer != null); assert(timer!.isActive); if (_terminal.supportsColor) { _writeToStdOut('\r\x1B[K'); // go to start of line and clear line } else { _clear(_currentLineLength); } _lastAnimationFrameLength = 0; timer?.cancel(); } @override void resume() { assert(timer != null); assert(!timer!.isActive); _startSpinner(); } @override void finish() { assert(timer != null); assert(timer!.isActive); timer?.cancel(); timer = null; _clear(_lastAnimationFrameLength); _lastAnimationFrameLength = 0; super.finish(); } } /// An animated version of [Status]. /// /// The constructor writes [message] to [stdout] with padding, then starts an /// indeterminate progress indicator animation. /// /// On [cancel] or [stop], will call [onFinish]. On [stop], will /// additionally print out summary information. /// /// Call [pause] before outputting any text while this is running. class SpinnerStatus extends AnonymousSpinnerStatus { SpinnerStatus({ required this.message, this.padding = kDefaultStatusPadding, super.onFinish, required super.stopwatch, required super.stdio, required super.terminal, }); final String message; final int padding; static final String _margin = AnonymousSpinnerStatus._clearChar * (5 + _kTimePadding - 1); int _totalMessageLength = 0; @override int get _currentLineLength => _totalMessageLength + super._currentLineLength; @override void start() { _printStatus(); super.start(); } void _printStatus() { final String line = '${message.padRight(padding)}$_margin'; _totalMessageLength = line.length; _writeToStdOut(line); } @override void pause() { super.pause(); _totalMessageLength = 0; } @override void resume() { _printStatus(); super.resume(); } @override void stop() { super.stop(); // calls finish, which clears the spinner assert(_totalMessageLength > _kTimePadding); _writeToStdOut(AnonymousSpinnerStatus._backspaceChar * (_kTimePadding - 1)); _writeToStdOut(elapsedTime.padLeft(_kTimePadding)); _writeToStdOut('\n'); } @override void cancel() { super.cancel(); // calls finish, which clears the spinner assert(_totalMessageLength > 0); _writeToStdOut('\n'); } }