Index: packages/cli_util/lib/cli_logging.dart |
diff --git a/packages/cli_util/lib/cli_logging.dart b/packages/cli_util/lib/cli_logging.dart |
new file mode 100644 |
index 0000000000000000000000000000000000000000..2da62ca723ad9fe374815793ebfcd9ae77c97774 |
--- /dev/null |
+++ b/packages/cli_util/lib/cli_logging.dart |
@@ -0,0 +1,273 @@ |
+// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file |
+// for details. All rights reserved. Use of this source code is governed by a |
+// BSD-style license that can be found in the LICENSE file. |
+ |
+/// This library contains functionality to help command-line utilities to easily |
+/// create aesthetic output. |
+library cli_logging; |
+ |
+import 'dart:async'; |
+import 'dart:io' as io; |
+ |
+/// A small utility class to make it easier to work with common ANSI escape |
+/// sequences. |
+class Ansi { |
+ /// Return whether the current stdout terminal supports ANSI escape sequences. |
+ static bool get terminalSupportsAnsi { |
+ return io.stdout.supportsAnsiEscapes && |
+ io.stdioType(io.stdout) == io.StdioType.TERMINAL; |
+ } |
+ |
+ final bool useAnsi; |
+ |
+ Ansi(this.useAnsi); |
+ |
+ String get cyan => _code('\u001b[36m'); |
+ String get green => _code('\u001b[32m'); |
+ String get magenta => _code('\u001b[35m'); |
+ String get red => _code('\u001b[31m'); |
+ String get yellow => _code('\u001b[33m'); |
+ String get blue => _code('\u001b[34m'); |
+ String get gray => _code('\u001b[1;30m'); |
+ String get noColor => _code('\u001b[39m'); |
+ |
+ String get none => _code('\u001b[0m'); |
+ |
+ String get bold => _code('\u001b[1m'); |
+ |
+ String get backspace => '\b'; |
+ |
+ String get bullet => io.stdout.supportsAnsiEscapes ? '•' : '-'; |
+ |
+ /// Display [message] in an emphasized format. |
+ String emphasized(String message) => '$bold$message$none'; |
+ |
+ /// Display [message] in an subtle (gray) format. |
+ String subtle(String message) => '$gray$message$none'; |
+ |
+ /// Display [message] in an error (red) format. |
+ String error(String message) => '$red$message$none'; |
+ |
+ String _code(String ansiCode) => useAnsi ? ansiCode : ''; |
+} |
+ |
+/// An abstract representation of a [Logger] - used to pretty print errors, |
+/// standard status messages, trace level output, and indeterminate progress. |
+abstract class Logger { |
+ /// Create a normal [Logger]; this logger will not display trace level output. |
+ factory Logger.standard({Ansi ansi}) => new _StandardLogger(ansi: ansi); |
+ |
+ /// Create a [Logger] that will display trace level output. |
+ factory Logger.verbose({Ansi ansi}) => new _VerboseLogger(ansi: ansi); |
+ |
+ Ansi get ansi; |
+ |
+ bool get isVerbose; |
+ |
+ /// Print an error message. |
+ void stderr(String message); |
+ |
+ /// Print a standard status message. |
+ void stdout(String message); |
+ |
+ /// Print trace output. |
+ void trace(String message); |
+ |
+ /// Start an indeterminate progress display. |
+ Progress progress(String message); |
+ void _progressFinished(Progress progress); |
+ |
+ /// Flush any un-written output. |
+ void flush(); |
+} |
+ |
+/// A handle to an indeterminate progress display. |
+abstract class Progress { |
+ final String message; |
+ final Stopwatch _stopwatch; |
+ |
+ Progress._(this.message) : _stopwatch = new Stopwatch()..start(); |
+ |
+ Duration get elapsed => _stopwatch.elapsed; |
+ |
+ /// Finish the indeterminate progress display. |
+ void finish({String message, bool showTiming}); |
+ |
+ /// Cancel the indeterminate progress display. |
+ void cancel(); |
+} |
+ |
+class _StandardLogger implements Logger { |
+ Ansi ansi; |
+ |
+ _StandardLogger({this.ansi}) { |
+ ansi ??= new Ansi(Ansi.terminalSupportsAnsi); |
+ } |
+ |
+ bool get isVerbose => false; |
+ |
+ Progress _currentProgress; |
+ |
+ void stderr(String message) { |
+ io.stderr.writeln(message); |
+ _currentProgress?.cancel(); |
+ _currentProgress = null; |
+ } |
+ |
+ void stdout(String message) { |
+ print(message); |
+ _currentProgress?.cancel(); |
+ _currentProgress = null; |
+ } |
+ |
+ void trace(String message) {} |
+ |
+ Progress progress(String message) { |
+ _currentProgress?.cancel(); |
+ _currentProgress = null; |
+ |
+ Progress progress = ansi.useAnsi |
+ ? new _AnsiProgress(this, ansi, message) |
+ : new _SimpleProgress(this, message); |
+ _currentProgress = progress; |
+ return progress; |
+ } |
+ |
+ void _progressFinished(Progress progress) { |
+ if (_currentProgress == progress) { |
+ _currentProgress = null; |
+ } |
+ } |
+ |
+ void flush() {} |
+} |
+ |
+class _SimpleProgress extends Progress { |
+ final Logger logger; |
+ |
+ _SimpleProgress(this.logger, String message) : super._(message) { |
+ logger.stdout('$message...'); |
+ } |
+ |
+ @override |
+ void cancel() { |
+ logger._progressFinished(this); |
+ } |
+ |
+ @override |
+ void finish({String message, bool showTiming}) { |
+ logger._progressFinished(this); |
+ } |
+} |
+ |
+class _AnsiProgress extends Progress { |
+ static const List<String> kAnimationItems = const ['/', '-', '\\', '|']; |
+ |
+ final Logger logger; |
+ final Ansi ansi; |
+ |
+ int _index = 0; |
+ Timer _timer; |
+ |
+ _AnsiProgress(this.logger, this.ansi, String message) : super._(message) { |
+ io.stdout.write('${message}... '.padRight(40)); |
+ |
+ _timer = new Timer.periodic(new Duration(milliseconds: 80), (t) { |
+ _index++; |
+ _updateDisplay(); |
+ }); |
+ |
+ _updateDisplay(); |
+ } |
+ |
+ @override |
+ void cancel() { |
+ if (_timer.isActive) { |
+ _timer.cancel(); |
+ _updateDisplay(cancelled: true); |
+ logger._progressFinished(this); |
+ } |
+ } |
+ |
+ @override |
+ void finish({String message, bool showTiming: false}) { |
+ if (_timer.isActive) { |
+ _timer.cancel(); |
+ _updateDisplay(isFinal: true, message: message, showTiming: showTiming); |
+ logger._progressFinished(this); |
+ } |
+ } |
+ |
+ void _updateDisplay( |
+ {bool isFinal: false, |
+ bool cancelled: false, |
+ String message, |
+ bool showTiming: false}) { |
+ String char = kAnimationItems[_index % kAnimationItems.length]; |
+ if (isFinal || cancelled) { |
+ char = ''; |
+ } |
+ io.stdout.write('${ansi.backspace}${char}'); |
+ if (isFinal || cancelled) { |
+ if (message != null) { |
+ io.stdout.write(message.isEmpty ? ' ' : message); |
+ } else if (showTiming) { |
+ String time = (elapsed.inMilliseconds / 1000.0).toStringAsFixed(1); |
+ io.stdout.write('${time}s'); |
+ } else { |
+ io.stdout.write(' '); |
+ } |
+ io.stdout.writeln(); |
+ } |
+ } |
+} |
+ |
+class _VerboseLogger implements Logger { |
+ Ansi ansi; |
+ Stopwatch _timer; |
+ |
+ String _previousErr; |
+ String _previousMsg; |
+ |
+ _VerboseLogger({this.ansi}) { |
+ ansi ??= new Ansi(Ansi.terminalSupportsAnsi); |
+ _timer = new Stopwatch()..start(); |
+ } |
+ |
+ bool get isVerbose => true; |
+ |
+ void stderr(String message) { |
+ flush(); |
+ _previousErr = '${ansi.red}$message${ansi.none}'; |
+ } |
+ |
+ void stdout(String message) { |
+ flush(); |
+ _previousMsg = message; |
+ } |
+ |
+ void trace(String message) { |
+ flush(); |
+ _previousMsg = '${ansi.gray}$message${ansi.none}'; |
+ } |
+ |
+ Progress progress(String message) => new _SimpleProgress(this, message); |
+ |
+ void _progressFinished(Progress progress) {} |
+ |
+ void flush() { |
+ if (_previousErr != null) { |
+ io.stderr.writeln('${_createTag()} $_previousErr'); |
+ _previousErr = null; |
+ } else if (_previousMsg != null) { |
+ io.stdout.writeln('${_createTag()} $_previousMsg'); |
+ _previousMsg = null; |
+ } |
+ } |
+ |
+ String _createTag() { |
+ int millis = _timer.elapsedMilliseconds; |
+ _timer.reset(); |
+ return '[${millis.toString().padLeft(4)} ms]'; |
+ } |
+} |