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
diff --git a/packages/flutter_tools/lib/runner.dart b/packages/flutter_tools/lib/runner.dart
index a8f7a9a..d727c70 100644
--- a/packages/flutter_tools/lib/runner.dart
+++ b/packages/flutter_tools/lib/runner.dart
@@ -75,22 +75,15 @@
     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.
diff --git a/packages/flutter_tools/lib/src/base/logger.dart b/packages/flutter_tools/lib/src/base/logger.dart
index a9d09d4..e97d222 100644
--- a/packages/flutter_tools/lib/src/base/logger.dart
+++ b/packages/flutter_tools/lib/src/base/logger.dart
@@ -27,14 +27,25 @@
 
   /// 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 @@
   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 @@
   }
 
   @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 @@
         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 @@
 /// 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 @@
   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 @@
   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 VerboseLogger extends Logger {
-  VerboseLogger(this.parent)
-    : assert(terminal != null) {
+  VerboseLogger(this.parent) : assert(terminal != null) {
     stopwatch.start();
   }
 
@@ -213,15 +250,23 @@
   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 @@
   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 @@
   }
 }
 
-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 @@
 /// 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 @@
 /// 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 @@
 /// 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 @@
     }
   }
 }
+
+/// 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));
+    }
+  }
+}
diff --git a/packages/flutter_tools/lib/src/base/terminal.dart b/packages/flutter_tools/lib/src/base/terminal.dart
index c6c21b9..8e40c59 100644
--- a/packages/flutter_tools/lib/src/base/terminal.dart
+++ b/packages/flutter_tools/lib/src/base/terminal.dart
@@ -20,19 +20,50 @@
       : context[AnsiTerminal];
 }
 
-class AnsiTerminal {
-  static const String _bold  = '\u001B[1m';
-  static const String _reset = '\u001B[0m';
-  static const String _clear = '\u001B[2J\u001B[H';
+enum TerminalColor {
+  red,
+  green,
+  blue,
+  cyan,
+  yellow,
+  magenta,
+  grey,
+}
 
-  bool supportsColor = platform.stdoutSupportsAnsi;
+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 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,
+  };
+
+  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 @@
         : 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 @@
     return choice;
   }
 }
-
diff --git a/packages/flutter_tools/lib/src/commands/daemon.dart b/packages/flutter_tools/lib/src/commands/daemon.dart
index 81337c9..be3b2b6 100644
--- a/packages/flutter_tools/lib/src/commands/daemon.dart
+++ b/packages/flutter_tools/lib/src/commands/daemon.dart
@@ -13,6 +13,7 @@
 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 @@
   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 @@
   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 @@
 
   @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});
     }
   }
 
diff --git a/packages/flutter_tools/lib/src/commands/fuchsia_reload.dart b/packages/flutter_tools/lib/src/commands/fuchsia_reload.dart
index 10ed98a..8dd0ab8 100644
--- a/packages/flutter_tools/lib/src/commands/fuchsia_reload.dart
+++ b/packages/flutter_tools/lib/src/commands/fuchsia_reload.dart
@@ -10,6 +10,7 @@
 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 @@
     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 @@
     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 @@
     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'
diff --git a/packages/flutter_tools/lib/src/compile.dart b/packages/flutter_tools/lib/src/compile.dart
index a98644c..3039c2c 100644
--- a/packages/flutter_tools/lib/src/compile.dart
+++ b/packages/flutter_tools/lib/src/compile.dart
@@ -13,11 +13,12 @@
 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;
diff --git a/packages/flutter_tools/lib/src/globals.dart b/packages/flutter_tools/lib/src/globals.dart
index b36eb1b..ca8f334 100644
--- a/packages/flutter_tools/lib/src/globals.dart
+++ b/packages/flutter_tools/lib/src/globals.dart
@@ -6,6 +6,7 @@
 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 @@
 /// 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 @@
 ///
 /// 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,
   );
 }
 
diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart
index b51f8b4..5cbac19 100644
--- a/packages/flutter_tools/lib/src/ios/mac.dart
+++ b/packages/flutter_tools/lib/src/ios/mac.dart
@@ -455,8 +455,7 @@
   // 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)}',
   );
 
diff --git a/packages/flutter_tools/lib/src/run_hot.dart b/packages/flutter_tools/lib/src/run_hot.dart
index f1c1a50..f973c89 100644
--- a/packages/flutter_tools/lib/src/run_hot.dart
+++ b/packages/flutter_tools/lib/src/run_hot.dart
@@ -13,6 +13,7 @@
 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 @@
   @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)
diff --git a/packages/flutter_tools/lib/src/test/flutter_platform.dart b/packages/flutter_tools/lib/src/test/flutter_platform.dart
index 695b235..28572f7 100644
--- a/packages/flutter_tools/lib/src/test/flutter_platform.dart
+++ b/packages/flutter_tools/lib/src/test/flutter_platform.dart
@@ -18,6 +18,7 @@
 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 @@
     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;
       }
diff --git a/packages/flutter_tools/test/base/logger_test.dart b/packages/flutter_tools/test/base/logger_test.dart
index fd8a1bd..ebf116c 100644
--- a/packages/flutter_tools/test/base/logger_test.dart
+++ b/packages/flutter_tools/test/base/logger_test.dart
@@ -7,12 +7,17 @@
 import 'package:flutter_tools/src/base/context.dart';
 import 'package:flutter_tools/src/base/io.dart';
 import 'package:flutter_tools/src/base/logger.dart';
+import 'package:flutter_tools/src/base/terminal.dart';
 
 import '../src/common.dart';
 import '../src/context.dart';
 import '../src/mocks.dart';
 
 void main() {
+  final String red = RegExp.escape(AnsiTerminal.red);
+  final String bold = RegExp.escape(AnsiTerminal.bold);
+  final String reset = RegExp.escape(AnsiTerminal.reset);
+
   group('AppContext', () {
     test('error', () async {
       final BufferLogger mockLogger = BufferLogger();
@@ -28,12 +33,32 @@
       expect(mockLogger.traceText, '');
       expect(mockLogger.errorText, matches( r'^\[ (?: {0,2}\+[0-9]{1,3} ms|       )\] Helpless!\n$'));
     });
+
+    test('ANSI colored errors', () async {
+      final BufferLogger mockLogger = BufferLogger();
+      final VerboseLogger verboseLogger = VerboseLogger(mockLogger);
+      verboseLogger.supportsColor = true;
+
+      verboseLogger.printStatus('Hey Hey Hey Hey');
+      verboseLogger.printTrace('Oooh, I do I do I do');
+      verboseLogger.printError('Helpless!');
+
+      expect(
+          mockLogger.statusText,
+          matches(r'^\[ (?: {0,2}\+[0-9]{1,3} ms|       )\] ' '${bold}Hey Hey Hey Hey$reset'
+                  r'\n\[ (?: {0,2}\+[0-9]{1,3} ms|       )\] Oooh, I do I do I do\n$'));
+      expect(mockLogger.traceText, '');
+      expect(
+          mockLogger.errorText,
+          matches('^$red' r'\[ (?: {0,2}\+[0-9]{1,3} ms|       )\] ' '${bold}Helpless!$reset$reset' r'\n$'));
+    });
   });
 
   group('Spinners', () {
     MockStdio mockStdio;
     AnsiSpinner ansiSpinner;
     AnsiStatus ansiStatus;
+    SummaryStatus summaryStatus;
     int called;
     final RegExp secondDigits = RegExp(r'[^\b]\b\b\b\b\b[0-9]+[.][0-9]+(?:s|ms)');
 
@@ -47,9 +72,16 @@
         padding: 20,
         onFinish: () => called++,
       );
+      summaryStatus = SummaryStatus(
+        message: 'Hello world',
+        expectSlowOperation: true,
+        padding: 20,
+        onFinish: () => called++,
+      );
     });
 
-    List<String> outputLines() => mockStdio.writtenToStdout.join('').split('\n');
+    List<String> outputStdout() => mockStdio.writtenToStdout.join('').split('\n');
+    List<String> outputStderr() => mockStdio.writtenToStderr.join('').split('\n');
 
     Future<void> doWhileAsync(bool doThis()) async {
       return Future.doWhile(() {
@@ -62,12 +94,12 @@
     testUsingContext('AnsiSpinner works', () async {
       ansiSpinner.start();
       await doWhileAsync(() => ansiSpinner.ticks < 10);
-      List<String> lines = outputLines();
+      List<String> lines = outputStdout();
       expect(lines[0], startsWith(' \b-\b\\\b|\b/\b-\b\\\b|\b/'));
       expect(lines[0].endsWith('\n'), isFalse);
       expect(lines.length, equals(1));
       ansiSpinner.stop();
-      lines = outputLines();
+      lines = outputStdout();
       expect(lines[0], endsWith('\b \b'));
       expect(lines.length, equals(1));
 
@@ -76,17 +108,95 @@
       expect(() { ansiSpinner.cancel(); }, throwsA(isInstanceOf<AssertionError>()));
     }, overrides: <Type, Generator>{Stdio: () => mockStdio});
 
+    testUsingContext('Error logs are red', () async {
+      context[Logger].printError('Pants on fire!');
+      final List<String> lines = outputStderr();
+      expect(outputStdout().length, equals(1));
+      expect(outputStdout().first, isEmpty);
+      expect(lines[0], equals('${AnsiTerminal.red}Pants on fire!${AnsiTerminal.reset}'));
+    }, overrides: <Type, Generator>{
+      Stdio: () => mockStdio,
+      Logger: () => StdoutLogger()..supportsColor = true,
+    });
+
+    testUsingContext('Stdout logs are not colored', () async {
+      context[Logger].printStatus('All good.');
+      final List<String> lines = outputStdout();
+      expect(outputStderr().length, equals(1));
+      expect(outputStderr().first, isEmpty);
+      expect(lines[0], equals('All good.'));
+    }, overrides: <Type, Generator>{
+      Stdio: () => mockStdio,
+      Logger: () => StdoutLogger()..supportsColor = true,
+    });
+
+    testUsingContext('Stdout printStatus handle null inputs on colored terminal', () async {
+      context[Logger].printStatus(null, emphasis: null,
+        color: null,
+        newline: null,
+        indent: null);
+      final List<String> lines = outputStdout();
+      expect(outputStderr().length, equals(1));
+      expect(outputStderr().first, isEmpty);
+      expect(lines[0], equals(''));
+    }, overrides: <Type, Generator>{
+      Stdio: () => mockStdio,
+      Logger: () => StdoutLogger()..supportsColor = true,
+    });
+
+    testUsingContext('Stdout startProgress handle null inputs on colored terminal', () async {
+      context[Logger].startProgress(null, progressId: null,
+        expectSlowOperation: null,
+        progressIndicatorPadding: null,
+      );
+      final List<String> lines = outputStdout();
+      expect(outputStderr().length, equals(1));
+      expect(outputStderr().first, isEmpty);
+      expect(lines[0], equals('                                                                 \b-'));
+    }, overrides: <Type, Generator>{
+      Stdio: () => mockStdio,
+      Logger: () => StdoutLogger()..supportsColor = true,
+    });
+
+    testUsingContext('Stdout printStatus handle null inputs on regular terminal', () async {
+      context[Logger].printStatus(null, emphasis: null,
+          color: null,
+          newline: null,
+          indent: null);
+      final List<String> lines = outputStdout();
+      expect(outputStderr().length, equals(1));
+      expect(outputStderr().first, isEmpty);
+      expect(lines[0], equals(''));
+    }, overrides: <Type, Generator>{
+      Stdio: () => mockStdio,
+      Logger: () => StdoutLogger()..supportsColor = false,
+    });
+
+    testUsingContext('Stdout startProgress handle null inputs on regular terminal', () async {
+      context[Logger].startProgress(null, progressId: null,
+        expectSlowOperation: null,
+        progressIndicatorPadding: null,
+      );
+      final List<String> lines = outputStdout();
+      expect(outputStderr().length, equals(1));
+      expect(outputStderr().first, isEmpty);
+      expect(lines[0], equals('                                                                '));
+    }, overrides: <Type, Generator>{
+      Stdio: () => mockStdio,
+      Logger: () => StdoutLogger()..supportsColor = false,
+    });
+
     testUsingContext('AnsiStatus works when cancelled', () async {
       ansiStatus.start();
       await doWhileAsync(() => ansiStatus.ticks < 10);
-      List<String> lines = outputLines();
+      List<String> lines = outputStdout();
       expect(lines[0], startsWith('Hello world               \b-\b\\\b|\b/\b-\b\\\b|\b/\b-'));
       expect(lines.length, equals(1));
       expect(lines[0].endsWith('\n'), isFalse);
 
       // Verify a cancel does _not_ print the time and prints a newline.
       ansiStatus.cancel();
-      lines = outputLines();
+      lines = outputStdout();
       final List<Match> matches = secondDigits.allMatches(lines[0]).toList();
       expect(matches, isEmpty);
       expect(lines[0], endsWith('\b \b'));
@@ -102,13 +212,13 @@
     testUsingContext('AnsiStatus works when stopped', () async {
       ansiStatus.start();
       await doWhileAsync(() => ansiStatus.ticks < 10);
-      List<String> lines = outputLines();
+      List<String> lines = outputStdout();
       expect(lines[0], startsWith('Hello world               \b-\b\\\b|\b/\b-\b\\\b|\b/\b-'));
       expect(lines.length, equals(1));
 
       // Verify a stop prints the time.
       ansiStatus.stop();
-      lines = outputLines();
+      lines = outputStdout();
       final List<Match> matches = secondDigits.allMatches(lines[0]).toList();
       expect(matches, isNotNull);
       expect(matches, hasLength(1));
@@ -123,23 +233,68 @@
       expect(() { ansiStatus.cancel(); }, throwsA(isInstanceOf<AssertionError>()));
     }, overrides: <Type, Generator>{Stdio: () => mockStdio});
 
+    testUsingContext('SummaryStatus works when cancelled', () async {
+      summaryStatus.start();
+      List<String> lines = outputStdout();
+      expect(lines[0], startsWith('Hello world              '));
+      expect(lines.length, equals(1));
+      expect(lines[0].endsWith('\n'), isFalse);
+
+      // Verify a cancel does _not_ print the time and prints a newline.
+      summaryStatus.cancel();
+      lines = outputStdout();
+      final List<Match> matches = secondDigits.allMatches(lines[0]).toList();
+      expect(matches, isEmpty);
+      expect(lines[0], endsWith(' '));
+      expect(called, equals(1));
+      expect(lines.length, equals(2));
+      expect(lines[1], equals(''));
+
+      // Verify that stopping or canceling multiple times throws.
+      expect(() { summaryStatus.cancel(); }, throwsA(isInstanceOf<AssertionError>()));
+      expect(() { summaryStatus.stop(); }, throwsA(isInstanceOf<AssertionError>()));
+    }, overrides: <Type, Generator>{Stdio: () => mockStdio});
+
+    testUsingContext('SummaryStatus works when stopped', () async {
+      summaryStatus.start();
+      List<String> lines = outputStdout();
+      expect(lines[0], startsWith('Hello world              '));
+      expect(lines.length, equals(1));
+
+      // Verify a stop prints the time.
+      summaryStatus.stop();
+      lines = outputStdout();
+      final List<Match> matches = secondDigits.allMatches(lines[0]).toList();
+      expect(matches, isNotNull);
+      expect(matches, hasLength(1));
+      final Match match = matches.first;
+      expect(lines[0], endsWith(match.group(0)));
+      expect(called, equals(1));
+      expect(lines.length, equals(2));
+      expect(lines[1], equals(''));
+
+      // Verify that stopping or canceling multiple times throws.
+      expect(() { summaryStatus.stop(); }, throwsA(isInstanceOf<AssertionError>()));
+      expect(() { summaryStatus.cancel(); }, throwsA(isInstanceOf<AssertionError>()));
+    }, overrides: <Type, Generator>{Stdio: () => mockStdio});
+
     testUsingContext('sequential startProgress calls with StdoutLogger', () async {
       context[Logger].startProgress('AAA')..stop();
       context[Logger].startProgress('BBB')..stop();
-      expect(outputLines(), <String>[
-        'AAA',
-        'BBB',
+      expect(outputStdout(), <String>[
+        'AAA                                                               0ms',
+        'BBB                                                               0ms',
         '',
       ]);
     }, overrides: <Type, Generator>{
       Stdio: () => mockStdio,
-      Logger: () => StdoutLogger(),
+      Logger: () => StdoutLogger()..supportsColor = false,
     });
 
     testUsingContext('sequential startProgress calls with VerboseLogger and StdoutLogger', () async {
       context[Logger].startProgress('AAA')..stop();
       context[Logger].startProgress('BBB')..stop();
-      expect(outputLines(), <Matcher>[
+      expect(outputStdout(), <Matcher>[
         matches(r'^\[ (?: {0,2}\+[0-9]{1,3} ms|       )\] AAA$'),
         matches(r'^\[ (?: {0,2}\+[0-9]{1,3} ms|       )\] AAA \(completed\)$'),
         matches(r'^\[ (?: {0,2}\+[0-9]{1,3} ms|       )\] BBB$'),
diff --git a/packages/flutter_tools/test/src/context.dart b/packages/flutter_tools/test/src/context.dart
index 743b623..b20bac2 100644
--- a/packages/flutter_tools/test/src/context.dart
+++ b/packages/flutter_tools/test/src/context.dart
@@ -76,7 +76,7 @@
             when(mock.getAttachedDevices()).thenReturn(<IOSSimulator>[]);
             return mock;
           },
-          Logger: () => BufferLogger(),
+          Logger: () => BufferLogger()..supportsColor = false,
           OperatingSystemUtils: () => MockOperatingSystemUtils(),
           SimControl: () => MockSimControl(),
           Usage: () => MockUsage(),
diff --git a/packages/flutter_tools/test/src/mocks.dart b/packages/flutter_tools/test/src/mocks.dart
index 408d111..d21cc91 100644
--- a/packages/flutter_tools/test/src/mocks.dart
+++ b/packages/flutter_tools/test/src/mocks.dart
@@ -287,12 +287,16 @@
 /// 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;
 
   void simulateStdin(String line) {
@@ -300,6 +304,7 @@
   }
 
   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 {