Unverified Commit 7caa6594 authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Added more extensive ANSI color printing support on terminals. (#20958)

This adds support to AnsiTerminal for colored output, and makes all tool output written to stderr (with the printError function) colored red.

No color codes are sent if the terminal doesn't support color (or isn't a terminal).

Also makes "progress" output print the elapsed time when not connected to a terminal, so that redirected output and terminal output match (redirected output doesn't print the spinner, however).

Addresses #17307
parent 85b4670b
......@@ -75,22 +75,15 @@ Future<int> _handleToolError(
String getFlutterVersion(),
) async {
if (error is UsageException) {
stderr.writeln(error.message);
stderr.writeln();
stderr.writeln(
"Run 'flutter -h' (or 'flutter <command> -h') for available "
'flutter commands and options.'
);
printError('${error.message}\n');
printError("Run 'flutter -h' (or 'flutter <command> -h') for available flutter commands and options.");
// Argument error exit code.
return _exit(64);
} else if (error is ToolExit) {
if (error.message != null)
stderr.writeln(error.message);
if (verbose) {
stderr.writeln();
stderr.writeln(stackTrace.toString());
stderr.writeln();
}
printError(error.message);
if (verbose)
printError('\n$stackTrace\n');
return _exit(error.exitCode ?? 1);
} else if (error is ProcessExit) {
// We've caught an exit code.
......
......@@ -27,14 +27,25 @@ abstract class Logger {
/// Display an error level message to the user. Commands should use this if they
/// fail in some way.
void printError(String message, { StackTrace stackTrace, bool emphasis = false });
void printError(
String message, {
StackTrace stackTrace,
bool emphasis,
TerminalColor color,
});
/// Display normal output of the command. This should be used for things like
/// progress messages, success messages, or just normal command output.
///
/// If [newline] is null, then it defaults to "true". If [emphasis] is null,
/// then it defaults to "false".
void printStatus(
String message,
{ bool emphasis = false, bool newline = true, String ansiAlternative, int indent }
);
String message, {
bool emphasis,
TerminalColor color,
bool newline,
int indent,
});
/// 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.
......@@ -50,43 +61,57 @@ abstract class Logger {
Status startProgress(
String message, {
String progressId,
bool expectSlowOperation = false,
int progressIndicatorPadding = kDefaultStatusPadding,
bool expectSlowOperation,
int progressIndicatorPadding,
});
}
class StdoutLogger extends Logger {
Status _status;
@override
bool get isVerbose => false;
@override
void printError(String message, { StackTrace stackTrace, bool emphasis = false }) {
void printError(
String message, {
StackTrace stackTrace,
bool emphasis,
TerminalColor color,
}) {
message ??= '';
_status?.cancel();
_status = null;
if (emphasis)
if (emphasis == true)
message = terminal.bolden(message);
message = terminal.color(message, color ?? TerminalColor.red);
stderr.writeln(message);
if (stackTrace != null)
if (stackTrace != null) {
stderr.writeln(stackTrace.toString());
}
}
@override
void printStatus(
String message,
{ bool emphasis = false, bool newline = true, String ansiAlternative, int indent }
) {
String message, {
bool emphasis,
TerminalColor color,
bool newline,
int indent,
}) {
message ??= '';
_status?.cancel();
_status = null;
if (terminal.supportsColor && ansiAlternative != null)
message = ansiAlternative;
if (emphasis)
if (emphasis == true)
message = terminal.bolden(message);
if (indent != null && indent > 0)
message = LineSplitter.split(message).map((String line) => ' ' * indent + line).join('\n');
if (newline)
if (color != null)
message = terminal.color(message, color);
if (indent != null && indent > 0) {
message = LineSplitter.split(message)
.map((String line) => ' ' * indent + line)
.join('\n');
}
if (newline != false)
message = '$message\n';
writeToStdOut(message);
}
......@@ -97,15 +122,17 @@ class StdoutLogger extends Logger {
}
@override
void printTrace(String message) { }
void printTrace(String message) {}
@override
Status startProgress(
String message, {
String progressId,
bool expectSlowOperation = false,
int progressIndicatorPadding = 59,
bool expectSlowOperation,
int progressIndicatorPadding,
}) {
expectSlowOperation ??= false;
progressIndicatorPadding ??= kDefaultStatusPadding;
if (_status != null) {
// Ignore nested progresses; return a no-op status object.
return Status(onFinish: _clearStatus)..start();
......@@ -118,8 +145,12 @@ class StdoutLogger extends Logger {
onFinish: _clearStatus,
)..start();
} else {
printStatus(message);
_status = Status(onFinish: _clearStatus)..start();
_status = SummaryStatus(
message: message,
expectSlowOperation: expectSlowOperation,
padding: progressIndicatorPadding,
onFinish: _clearStatus,
)..start();
}
return _status;
}
......@@ -138,7 +169,6 @@ class StdoutLogger extends Logger {
/// fonts, should be replaced by this class with printable symbols. Otherwise,
/// they will show up as the unrepresentable character symbol '�'.
class WindowsStdoutLogger extends StdoutLogger {
@override
void writeToStdOut(String message) {
// TODO(jcollins-g): wrong abstraction layer for this, move to [Stdio].
......@@ -162,16 +192,24 @@ class BufferLogger extends Logger {
String get traceText => _trace.toString();
@override
void printError(String message, { StackTrace stackTrace, bool emphasis = false }) {
_error.writeln(message);
void printError(
String message, {
StackTrace stackTrace,
bool emphasis,
TerminalColor color,
}) {
_error.writeln(terminal.color(message, color ?? TerminalColor.red));
}
@override
void printStatus(
String message,
{ bool emphasis = false, bool newline = true, String ansiAlternative, int indent }
) {
if (newline)
String message, {
bool emphasis,
TerminalColor color,
bool newline,
int indent,
}) {
if (newline != false)
_status.writeln(message);
else
_status.write(message);
......@@ -184,8 +222,8 @@ class BufferLogger extends Logger {
Status startProgress(
String message, {
String progressId,
bool expectSlowOperation = false,
int progressIndicatorPadding = kDefaultStatusPadding,
bool expectSlowOperation,
int progressIndicatorPadding,
}) {
printStatus(message);
return Status()..start();
......@@ -200,8 +238,7 @@ class BufferLogger extends Logger {
}
class VerboseLogger extends Logger {
VerboseLogger(this.parent)
: assert(terminal != null) {
VerboseLogger(this.parent) : assert(terminal != null) {
stopwatch.start();
}
......@@ -213,15 +250,23 @@ class VerboseLogger extends Logger {
bool get isVerbose => true;
@override
void printError(String message, { StackTrace stackTrace, bool emphasis = false }) {
void printError(
String message, {
StackTrace stackTrace,
bool emphasis,
TerminalColor color,
}) {
_emit(_LogType.error, message, stackTrace);
}
@override
void printStatus(
String message,
{ bool emphasis = false, bool newline = true, String ansiAlternative, int indent }
) {
String message, {
bool emphasis,
TerminalColor color,
bool newline,
int indent,
}) {
_emit(_LogType.status, message);
}
......@@ -234,8 +279,8 @@ class VerboseLogger extends Logger {
Status startProgress(
String message, {
String progressId,
bool expectSlowOperation = false,
int progressIndicatorPadding = kDefaultStatusPadding,
bool expectSlowOperation,
int progressIndicatorPadding,
}) {
printStatus(message);
return Status(onFinish: () {
......@@ -276,11 +321,7 @@ class VerboseLogger extends Logger {
}
}
enum _LogType {
error,
status,
trace
}
enum _LogType { error, status, trace }
/// A [Status] class begins when start is called, and may produce progress
/// information asynchronously.
......@@ -297,7 +338,7 @@ enum _LogType {
/// Generally, consider `logger.startProgress` instead of directly creating
/// a [Status] or one of its subclasses.
class Status {
Status({ this.onFinish });
Status({this.onFinish});
/// A straight [Status] or an [AnsiSpinner] (depending on whether the
/// terminal is fancy enough), already started.
......@@ -337,7 +378,7 @@ class Status {
/// An [AnsiSpinner] is a simple animation that does nothing but implement an
/// ASCII spinner. When stopped or canceled, the animation erases itself.
class AnsiSpinner extends Status {
AnsiSpinner({ VoidCallback onFinish }) : super(onFinish: onFinish);
AnsiSpinner({VoidCallback onFinish}) : super(onFinish: onFinish);
int ticks = 0;
Timer timer;
......@@ -380,11 +421,14 @@ class AnsiSpinner extends Status {
/// milliseconds if [expectSlowOperation] is false, as seconds otherwise.
class AnsiStatus extends AnsiSpinner {
AnsiStatus({
this.message,
this.expectSlowOperation,
this.padding,
String message,
bool expectSlowOperation,
int padding,
VoidCallback onFinish,
}) : super(onFinish: onFinish);
}) : message = message ?? '',
padding = padding ?? 0,
expectSlowOperation = expectSlowOperation ?? false,
super(onFinish: onFinish);
final String message;
final bool expectSlowOperation;
......@@ -426,3 +470,56 @@ class AnsiStatus extends AnsiSpinner {
}
}
}
/// Constructor writes [message] to [stdout]. On [cancel] or [stop], will call
/// [onFinish]. On [stop], will additionally print out summary information in
/// milliseconds if [expectSlowOperation] is false, as seconds otherwise.
class SummaryStatus extends Status {
SummaryStatus({
String message,
bool expectSlowOperation,
int padding,
VoidCallback onFinish,
}) : message = message ?? '',
padding = padding ?? 0,
expectSlowOperation = expectSlowOperation ?? false,
super(onFinish: onFinish);
final String message;
final bool expectSlowOperation;
final int padding;
Stopwatch stopwatch;
@override
void start() {
stopwatch = Stopwatch()..start();
stdout.write('${message.padRight(padding)} ');
super.start();
}
@override
void stop() {
super.stop();
writeSummaryInformation();
stdout.write('\n');
}
@override
void cancel() {
super.cancel();
stdout.write('\n');
}
/// Prints a (minimum) 5 character padded time. If [expectSlowOperation] is
/// true, the time is in seconds; otherwise, milliseconds.
///
/// Example: ' 0.5s', '150ms', '1600ms'
void writeSummaryInformation() {
if (expectSlowOperation) {
stdout.write(getElapsedAsSeconds(stopwatch.elapsed).padLeft(5));
} else {
stdout.write(getElapsedAsMilliseconds(stopwatch.elapsed).padLeft(5));
}
}
}
......@@ -20,19 +20,50 @@ AnsiTerminal get terminal {
: context[AnsiTerminal];
}
enum TerminalColor {
red,
green,
blue,
cyan,
yellow,
magenta,
grey,
}
class AnsiTerminal {
static const String _bold = '\u001B[1m';
static const String _reset = '\u001B[0m';
static const String _clear = '\u001B[2J\u001B[H';
static const String bold = '\u001B[1m';
static const String reset = '\u001B[0m';
static const String clear = '\u001B[2J\u001B[H';
static const String red = '\u001b[31m';
static const String green = '\u001b[32m';
static const String blue = '\u001b[34m';
static const String cyan = '\u001b[36m';
static const String magenta = '\u001b[35m';
static const String yellow = '\u001b[33m';
static const String grey = '\u001b[1;30m';
static const Map<TerminalColor, String> _colorMap = <TerminalColor, String>{
TerminalColor.red: red,
TerminalColor.green: green,
TerminalColor.blue: blue,
TerminalColor.cyan: cyan,
TerminalColor.magenta: magenta,
TerminalColor.yellow: yellow,
TerminalColor.grey: grey,
};
bool supportsColor = platform.stdoutSupportsAnsi;
static String colorCode(TerminalColor color) => _colorMap[color];
bool supportsColor = platform.stdoutSupportsAnsi ?? false;
String bolden(String message) {
if (!supportsColor)
assert(message != null);
if (!supportsColor || message.isEmpty)
return message;
final StringBuffer buffer = StringBuffer();
for (String line in message.split('\n'))
buffer.writeln('$_bold$line$_reset');
buffer.writeln('$bold$line$reset');
final String result = buffer.toString();
// avoid introducing a new newline to the emboldened text
return (!message.endsWith('\n') && result.endsWith('\n'))
......@@ -40,7 +71,21 @@ class AnsiTerminal {
: result;
}
String clearScreen() => supportsColor ? _clear : '\n\n';
String color(String message, TerminalColor color) {
assert(message != null);
if (!supportsColor || color == null || message.isEmpty)
return message;
final StringBuffer buffer = StringBuffer();
for (String line in message.split('\n'))
buffer.writeln('${_colorMap[color]}$line$reset');
final String result = buffer.toString();
// avoid introducing a new newline to the colored text
return (!message.endsWith('\n') && result.endsWith('\n'))
? result.substring(0, result.length - 1)
: result;
}
String clearScreen() => supportsColor ? clear : '\n\n';
set singleCharMode(bool value) {
final Stream<List<int>> stdin = io.stdin;
......@@ -113,4 +158,3 @@ class AnsiTerminal {
return choice;
}
}
......@@ -13,6 +13,7 @@ import '../base/context.dart';
import '../base/file_system.dart';
import '../base/io.dart';
import '../base/logger.dart';
import '../base/terminal.dart';
import '../base/utils.dart';
import '../build_info.dart';
import '../cache.dart';
......@@ -746,15 +747,18 @@ class NotifyingLogger extends Logger {
Stream<LogMessage> get onMessage => _messageController.stream;
@override
void printError(String message, { StackTrace stackTrace, bool emphasis = false }) {
void printError(String message, { StackTrace stackTrace, bool emphasis = false, TerminalColor color }) {
_messageController.add(LogMessage('error', message, stackTrace));
}
@override
void printStatus(
String message,
{ bool emphasis = false, bool newline = true, String ansiAlternative, int indent }
) {
String message, {
bool emphasis = false,
TerminalColor color,
bool newline = true,
int indent,
}) {
_messageController.add(LogMessage('status', message));
}
......@@ -868,7 +872,7 @@ class _AppRunLogger extends Logger {
int _nextProgressId = 0;
@override
void printError(String message, { StackTrace stackTrace, bool emphasis = false }) {
void printError(String message, { StackTrace stackTrace, bool emphasis, TerminalColor color}) {
if (parent != null) {
parent.printError(message, stackTrace: stackTrace, emphasis: emphasis);
} else {
......@@ -889,14 +893,22 @@ class _AppRunLogger extends Logger {
@override
void printStatus(
String message, {
bool emphasis = false, bool newline = true, String ansiAlternative, int indent
}) {
String message, {
bool emphasis = false,
TerminalColor color,
bool newline = true,
int indent,
}) {
if (parent != null) {
parent.printStatus(message, emphasis: emphasis, newline: newline,
ansiAlternative: ansiAlternative, indent: indent);
parent.printStatus(
message,
emphasis: emphasis,
color: color,
newline: newline,
indent: indent,
);
} else {
_sendLogEvent(<String, dynamic>{ 'log': message });
_sendLogEvent(<String, dynamic>{'log': message});
}
}
......
......@@ -10,6 +10,7 @@ import '../base/common.dart';
import '../base/file_system.dart';
import '../base/io.dart';
import '../base/process_manager.dart';
import '../base/terminal.dart';
import '../base/utils.dart';
import '../bundle.dart' as bundle;
import '../cache.dart';
......@@ -188,9 +189,6 @@ class FuchsiaReloadCommand extends FlutterCommand {
return result;
}
static const String _bold = '\u001B[0;1m';
static const String _reset = '\u001B[0m';
String _vmServiceToString(VMService vmService, {int tabDepth = 0}) {
final Uri addr = vmService.httpAddress;
final String embedder = vmService.vm.embedder;
......@@ -218,7 +216,7 @@ class FuchsiaReloadCommand extends FlutterCommand {
final String tabs = '\t' * tabDepth;
final String extraTabs = '\t' * (tabDepth + 1);
final StringBuffer stringBuffer = StringBuffer(
'$tabs$_bold$embedder at $addr$_reset\n'
'$tabs${terminal.bolden('$embedder at $addr')}\n'
'${extraTabs}RSS: $maxRSS\n'
'${extraTabs}Native allocations: $heapSize\n'
'${extraTabs}New Spaces: $newUsed of $newCap\n'
......@@ -257,7 +255,7 @@ class FuchsiaReloadCommand extends FlutterCommand {
final String tabs = '\t' * tabDepth;
final String extraTabs = '\t' * (tabDepth + 1);
return
'$tabs$_bold$shortName$_reset\n'
'$tabs${terminal.bolden(shortName)}\n'
'${extraTabs}Isolate number: $number\n'
'${extraTabs}Observatory: $isolateAddr\n'
'${extraTabs}Debugger: $debuggerAddr\n'
......
......@@ -13,11 +13,12 @@ import 'base/context.dart';
import 'base/fingerprint.dart';
import 'base/io.dart';
import 'base/process_manager.dart';
import 'base/terminal.dart';
import 'globals.dart';
KernelCompiler get kernelCompiler => context[KernelCompiler];
typedef CompilerMessageConsumer = void Function(String message);
typedef CompilerMessageConsumer = void Function(String message, {bool emphasis, TerminalColor color});
class CompilerOutput {
final String outputFilename;
......
......@@ -6,6 +6,7 @@ import 'artifacts.dart';
import 'base/config.dart';
import 'base/context.dart';
import 'base/logger.dart';
import 'base/terminal.dart';
import 'cache.dart';
Logger get logger => context[Logger];
......@@ -16,9 +17,21 @@ Artifacts get artifacts => Artifacts.instance;
/// Display an error level message to the user. Commands should use this if they
/// fail in some way.
///
/// Set `emphasis` to true to make the output bold if it's supported.
void printError(String message, { StackTrace stackTrace, bool emphasis = false }) {
logger.printError(message, stackTrace: stackTrace, emphasis: emphasis);
/// Set [emphasis] to true to make the output bold if it's supported.
/// Set [color] to a [TerminalColor] to color the output, if the logger
/// supports it. The [color] defaults to [TerminalColor.red].
void printError(
String message, {
StackTrace stackTrace,
bool emphasis,
TerminalColor color,
}) {
logger.printError(
message,
stackTrace: stackTrace,
emphasis: emphasis ?? false,
color: color,
);
}
/// Display normal output of the command. This should be used for things like
......@@ -28,20 +41,21 @@ void printError(String message, { StackTrace stackTrace, bool emphasis = false }
///
/// Set `newline` to false to skip the trailing linefeed.
///
/// If `ansiAlternative` is provided, and the terminal supports color, that
/// string will be printed instead of the message.
///
/// If `indent` is provided, each line of the message will be prepended by the specified number of
/// whitespaces.
/// If `indent` is provided, each line of the message will be prepended by the
/// specified number of whitespaces.
void printStatus(
String message,
{ bool emphasis = false, bool newline = true, String ansiAlternative, int indent }) {
String message, {
bool emphasis,
bool newline,
TerminalColor color,
int indent,
}) {
logger.printStatus(
message,
emphasis: emphasis,
newline: newline,
ansiAlternative: ansiAlternative,
indent: indent
emphasis: emphasis ?? false,
color: color,
newline: newline ?? true,
indent: indent,
);
}
......
......@@ -455,8 +455,7 @@ Future<XcodeBuildResult> buildXcodeProject({
// Free pipe file.
tempDir?.deleteSync(recursive: true);
printStatus(
'Xcode build done.',
ansiAlternative: 'Xcode build done.'.padRight(kDefaultStatusPadding + 1)
'Xcode build done.'.padRight(kDefaultStatusPadding + 1)
+ '${getElapsedAsSeconds(buildStopwatch.elapsed).padLeft(5)}',
);
......
......@@ -13,6 +13,7 @@ import 'base/common.dart';
import 'base/context.dart';
import 'base/file_system.dart';
import 'base/logger.dart';
import 'base/terminal.dart';
import 'base/utils.dart';
import 'build_info.dart';
import 'compile.dart';
......@@ -740,14 +741,12 @@ class HotRunner extends ResidentRunner {
@override
void printHelp({ @required bool details }) {
const String fire = '🔥';
const String red = '\u001B[31m';
const String bold = '\u001B[0;1m';
const String reset = '\u001B[0m';
printStatus(
'$fire To hot reload changes while running, press "r". To hot restart (and rebuild state), press "R".',
ansiAlternative: '$red$fire$bold To hot reload changes while running, press "r". '
'To hot restart (and rebuild state), press "R".$reset'
final String message = terminal.color(
fire + terminal.bolden(' To hot reload changes while running, press "r". '
'To hot restart (and rebuild state), press "R".'),
TerminalColor.red,
);
printStatus(message);
for (FlutterDevice device in flutterDevices) {
final String dname = device.device.name;
for (Uri uri in device.observatoryUris)
......
......@@ -18,6 +18,7 @@ import '../base/common.dart';
import '../base/file_system.dart';
import '../base/io.dart';
import '../base/process_manager.dart';
import '../base/terminal.dart';
import '../build_info.dart';
import '../compile.dart';
import '../dart/package_map.dart';
......@@ -212,13 +213,17 @@ class _Compiler {
printTrace('Compiler will use the following file as its incremental dill file: ${outputDill.path}');
bool suppressOutput = false;
void reportCompilerMessage(String message) {
void reportCompilerMessage(String message, {bool emphasis, TerminalColor color}) {
if (suppressOutput)
return;
if (message.startsWith('compiler message: Error: Could not resolve the package \'test\'')) {
printTrace(message);
printError('\n\nFailed to load test harness. Are you missing a dependency on flutter_test?\n');
printError(
'\n\nFailed to load test harness. Are you missing a dependency on flutter_test?\n',
emphasis: emphasis,
color: color,
);
suppressOutput = true;
return;
}
......
......@@ -76,7 +76,7 @@ void testUsingContext(String description, dynamic testMethod(), {
when(mock.getAttachedDevices()).thenReturn(<IOSSimulator>[]);
return mock;
},
Logger: () => BufferLogger(),
Logger: () => BufferLogger()..supportsColor = false,
OperatingSystemUtils: () => MockOperatingSystemUtils(),
SimControl: () => MockSimControl(),
Usage: () => MockUsage(),
......
......@@ -287,11 +287,15 @@ class MemoryIOSink implements IOSink {
/// A Stdio that collects stdout and supports simulated stdin.
class MockStdio extends Stdio {
final MemoryIOSink _stdout = MemoryIOSink();
final MemoryIOSink _stderr = MemoryIOSink();
final StreamController<List<int>> _stdin = StreamController<List<int>>();
@override
IOSink get stdout => _stdout;
@override
IOSink get stderr => _stderr;
@override
Stream<List<int>> get stdin => _stdin.stream;
......@@ -300,6 +304,7 @@ class MockStdio extends Stdio {
}
List<String> get writtenToStdout => _stdout.writes.map(_stdout.encoding.decode).toList();
List<String> get writtenToStderr => _stderr.writes.map(_stderr.encoding.decode).toList();
}
class MockPollingDeviceDiscovery extends PollingDeviceDiscovery {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment