| // 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 'package:meta/meta.dart'; |
| |
| import '../convert.dart'; |
| import 'io.dart'; |
| import 'terminal.dart' show Terminal, TerminalColor, OutputPreferences; |
| 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 { |
| bool get isVerbose => false; |
| |
| bool quiet = false; |
| |
| bool get supportsColor; |
| |
| bool get hasTerminal; |
| |
| Terminal get terminal; |
| |
| OutputPreferences get _outputPreferences; |
| |
| /// Display an error `message` to the user. Commands should use this if they |
| /// fail in some way. |
| /// |
| /// The `message` argument is printed to the stderr in 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 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 stderr in red by default. |
| /// |
| /// 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, |
| }); |
| |
| /// 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 }); |
| |
| /// 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(); |
| } |
| |
| /// A [Logger] that forwards all methods to another one. |
| /// |
| /// 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 |
| 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 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 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}) { |
| return _delegate.startSpinner(onFinish: onFinish); |
| } |
| |
| @override |
| bool get supportsColor => _delegate.supportsColor; |
| |
| @override |
| void clear() => _delegate.clear(); |
| } |
| |
| /// 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, |
| }) { |
| _status?.pause(); |
| message = wrapText(message, |
| indent: indent, |
| hangingIndent: hangingIndent, |
| shouldWrap: wrap ?? _outputPreferences.wrapText, |
| columnWidth: _outputPreferences.wrapColumn, |
| ); |
| if (emphasis == true) { |
| message = terminal.bolden(message); |
| } |
| message = terminal.color(message, color ?? TerminalColor.red); |
| writeToStdErr('$message\n'); |
| if (stackTrace != null) { |
| writeToStdErr('$stackTrace\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 == true) { |
| message = terminal.bolden(message); |
| } |
| if (color != null) { |
| message = terminal.color(message, color); |
| } |
| if (newline != false) { |
| message = '$message\n'; |
| } |
| writeToStdOut(message); |
| _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 }) { |
| 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, |
| )..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(); |
| } |
| } |
| |
| /// 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 Terminal terminal, |
| required Stdio stdio, |
| required OutputPreferences outputPreferences, |
| StopwatchFactory stopwatchFactory = const StopwatchFactory(), |
| }) : super( |
| terminal: terminal, |
| stdio: stdio, |
| outputPreferences: outputPreferences, |
| stopwatchFactory: stopwatchFactory, |
| ); |
| |
| @override |
| void writeToStdOut(String message) { |
| final String windowsMessage = terminal.supportsEmoji |
| ? message |
| : message.replaceAll('🔥', '') |
| .replaceAll('🖼️', '') |
| .replaceAll('✗', 'X') |
| .replaceAll('✓', '√') |
| .replaceAll('🔨', '') |
| .replaceAll('💪', '') |
| .replaceAll('✏️', ''); |
| _stdio.stdoutWrite(windowsMessage); |
| } |
| } |
| |
| class BufferLogger extends Logger { |
| BufferLogger({ |
| required this.terminal, |
| required OutputPreferences outputPreferences, |
| StopwatchFactory stopwatchFactory = const StopwatchFactory(), |
| }) : _outputPreferences = outputPreferences, |
| _stopwatchFactory = stopwatchFactory; |
| |
| /// Create a [BufferLogger] with test preferences. |
| BufferLogger.test({ |
| Terminal? terminal, |
| OutputPreferences? outputPreferences, |
| }) : terminal = terminal ?? Terminal.test(), |
| _outputPreferences = outputPreferences ?? OutputPreferences.test(), |
| _stopwatchFactory = const StopwatchFactory(); |
| |
| |
| @override |
| final OutputPreferences _outputPreferences; |
| |
| @override |
| final Terminal terminal; |
| |
| final StopwatchFactory _stopwatchFactory; |
| |
| @override |
| bool get isVerbose => false; |
| |
| @override |
| bool get supportsColor => terminal.supportsColor; |
| |
| final StringBuffer _error = StringBuffer(); |
| final StringBuffer _status = StringBuffer(); |
| final StringBuffer _trace = StringBuffer(); |
| final StringBuffer _events = StringBuffer(); |
| |
| String get errorText => _error.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, |
| }) { |
| _error.writeln(terminal.color( |
| wrapText(message, |
| indent: indent, |
| hangingIndent: hangingIndent, |
| shouldWrap: wrap ?? _outputPreferences.wrapText, |
| columnWidth: _outputPreferences.wrapColumn, |
| ), |
| color ?? TerminalColor.red, |
| )); |
| } |
| |
| @override |
| void printStatus( |
| String message, { |
| bool? emphasis, |
| TerminalColor? color, |
| bool? newline, |
| int? indent, |
| int? hangingIndent, |
| bool? wrap, |
| }) { |
| if (newline != false) { |
| _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 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}) { |
| 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(Logger parent, { |
| StopwatchFactory stopwatchFactory = const StopwatchFactory() |
| }) : _stopwatch = stopwatchFactory.createStopwatch(), |
| _stopwatchFactory = stopwatchFactory, |
| super(parent) { |
| _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, |
| }) { |
| _emit( |
| _LogType.error, |
| 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 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'); |
| |
| if (type == _LogType.error) { |
| super.printError(prefix + terminal.bolden(indentMessage)); |
| if (stackTrace != null) { |
| super.printError(indent + stackTrace.toString().replaceAll('\n', '\n$indent')); |
| } |
| } else if (type == _LogType.status) { |
| super.printStatus(prefix + terminal.bolden(indentMessage)); |
| } else { |
| super.printStatus(prefix + indentMessage); |
| } |
| } |
| |
| @override |
| void sendEvent(String name, [Map<String, dynamic>? args]) { } |
| } |
| |
| class PrefixedErrorLogger extends DelegatingLogger { |
| PrefixedErrorLogger(Logger parent) : super(parent); |
| |
| @override |
| void printError( |
| String message, { |
| StackTrace? stackTrace, |
| bool? emphasis, |
| TerminalColor? color, |
| int? indent, |
| int? hangingIndent, |
| bool? wrap, |
| }) { |
| 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, 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, |
| }) : _stopwatch = stopwatch; |
| |
| final VoidCallback? onFinish; |
| |
| @protected |
| final Stopwatch _stopwatch; |
| |
| @protected |
| String get elapsedTime { |
| if (_stopwatch.elapsed.inSeconds > 2) { |
| return getElapsedAsSeconds(_stopwatch.elapsed); |
| } |
| return getElapsedAsMilliseconds(_stopwatch.elapsed); |
| } |
| |
| /// 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 [SilentStatus] shows nothing. |
| class SilentStatus extends Status { |
| SilentStatus({ |
| required Stopwatch stopwatch, |
| VoidCallback? onFinish, |
| }) : super( |
| onFinish: onFinish, |
| stopwatch: stopwatch, |
| ); |
| |
| @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 Stopwatch stopwatch, |
| this.padding = kDefaultStatusPadding, |
| VoidCallback? onFinish, |
| required Stdio stdio, |
| }) : _stdio = stdio, |
| super( |
| onFinish: onFinish, |
| stopwatch: stopwatch, |
| ); |
| |
| 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({ |
| VoidCallback? onFinish, |
| required Stopwatch stopwatch, |
| required Stdio stdio, |
| required Terminal terminal, |
| }) : _stdio = stdio, |
| _terminal = terminal, |
| _animation = _selectAnimation(terminal), |
| super( |
| onFinish: onFinish, |
| stopwatch: stopwatch, |
| ); |
| |
| final Stdio _stdio; |
| final Terminal _terminal; |
| |
| 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; |
| |
| String get _currentAnimationFrame => _animation[ticks % _animation.length]; |
| int get _currentLineLength => _lastAnimationFrameLength; |
| |
| 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; |
| 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, |
| VoidCallback? onFinish, |
| required Stopwatch stopwatch, |
| required Stdio stdio, |
| required Terminal terminal, |
| }) : super( |
| onFinish: onFinish, |
| stopwatch: stopwatch, |
| stdio: stdio, |
| terminal: 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'); |
| } |
| } |