mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
1473 lines
39 KiB
Dart
1473 lines
39 KiB
Dart
// 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 '<27>'.
|
||
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;
|
||
_error.writeln(terminal.color(
|
||
wrapText(message,
|
||
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');
|
||
}
|
||
}
|