| // 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'); |
| } |
| } |