| // 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; | |
| 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 => | |
| 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 reversed => _code('\u001b[7m'); | |
| 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}) => StandardLogger(ansi: ansi); | |
| /// Create a [Logger] that will display trace level output. | |
| /// | |
| /// If [logTime] is `true`, this logger will display the time of the message. | |
| factory Logger.verbose({Ansi? ansi, bool logTime = true}) => | |
| VerboseLogger(ansi: ansi, logTime: logTime); | |
| 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); | |
| /// Print text to stdout, without a trailing newline. | |
| void write(String message); | |
| /// Print a character code to stdout, without a trailing newline. | |
| void writeCharCode(int charCode); | |
| /// Start an indeterminate progress display. | |
| Progress progress(String message); | |
| /// Flush any un-written output. | |
| @Deprecated('This method will be removed in the future') | |
| void flush(); | |
| } | |
| /// A handle to an indeterminate progress display. | |
| abstract class Progress { | |
| final String message; | |
| final Stopwatch _stopwatch; | |
| Progress(this.message) : _stopwatch = Stopwatch()..start(); | |
| Duration get elapsed => _stopwatch.elapsed; | |
| /// Finish the indeterminate progress display. | |
| void finish({String? message, bool showTiming = false}); | |
| /// Cancel the indeterminate progress display. | |
| void cancel(); | |
| } | |
| class StandardLogger implements Logger { | |
| @override | |
| Ansi ansi; | |
| StandardLogger({Ansi? ansi}) : ansi = ansi ?? Ansi(Ansi.terminalSupportsAnsi); | |
| @override | |
| bool get isVerbose => false; | |
| Progress? _currentProgress; | |
| @override | |
| void stderr(String message) { | |
| _cancelProgress(); | |
| io.stderr.writeln(message); | |
| } | |
| @override | |
| void stdout(String message) { | |
| _cancelProgress(); | |
| print(message); | |
| } | |
| @override | |
| void trace(String message) {} | |
| @override | |
| void write(String message) { | |
| _cancelProgress(); | |
| io.stdout.write(message); | |
| } | |
| @override | |
| void writeCharCode(int charCode) { | |
| _cancelProgress(); | |
| io.stdout.writeCharCode(charCode); | |
| } | |
| void _cancelProgress() { | |
| final progress = _currentProgress; | |
| if (progress != null) { | |
| _currentProgress = null; | |
| progress.cancel(); | |
| } | |
| } | |
| @override | |
| Progress progress(String message) { | |
| _cancelProgress(); | |
| final progress = | |
| ansi.useAnsi | |
| ? AnsiProgress(ansi, message) | |
| : SimpleProgress(this, message); | |
| _currentProgress = progress; | |
| return progress; | |
| } | |
| @override | |
| @Deprecated('This method will be removed in the future') | |
| void flush() {} | |
| } | |
| class SimpleProgress extends Progress { | |
| final Logger logger; | |
| SimpleProgress(this.logger, String message) : super(message) { | |
| logger.stdout('$message...'); | |
| } | |
| @override | |
| void cancel() {} | |
| @override | |
| void finish({String? message, bool showTiming = false}) {} | |
| } | |
| class AnsiProgress extends Progress { | |
| static const List<String> kAnimationItems = ['/', '-', r'\', '|']; | |
| final Ansi ansi; | |
| late final Timer _timer; | |
| AnsiProgress(this.ansi, String message) : super(message) { | |
| _timer = Timer.periodic(const Duration(milliseconds: 80), (t) { | |
| _updateDisplay(); | |
| }); | |
| io.stdout.write('$message... '.padRight(40)); | |
| _updateDisplay(); | |
| } | |
| @override | |
| void cancel() { | |
| if (_timer.isActive) { | |
| _timer.cancel(); | |
| _updateDisplay(cancelled: true); | |
| } | |
| } | |
| @override | |
| void finish({String? message, bool showTiming = false}) { | |
| if (_timer.isActive) { | |
| _timer.cancel(); | |
| _updateDisplay(isFinal: true, message: message, showTiming: showTiming); | |
| } | |
| } | |
| void _updateDisplay({ | |
| bool isFinal = false, | |
| bool cancelled = false, | |
| String? message, | |
| bool showTiming = false, | |
| }) { | |
| var char = kAnimationItems[_timer.tick % 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) { | |
| final time = (elapsed.inMilliseconds / 1000.0).toStringAsFixed(1); | |
| io.stdout.write('${time}s'); | |
| } else { | |
| io.stdout.write(' '); | |
| } | |
| io.stdout.writeln(); | |
| } | |
| } | |
| } | |
| class VerboseLogger implements Logger { | |
| @override | |
| Ansi ansi; | |
| bool logTime; | |
| final _timer = Stopwatch()..start(); | |
| VerboseLogger({Ansi? ansi, this.logTime = false}) | |
| : ansi = ansi ?? Ansi(Ansi.terminalSupportsAnsi); | |
| @override | |
| bool get isVerbose => true; | |
| @override | |
| void stdout(String message) { | |
| io.stdout.writeln('${_createPrefix()}$message'); | |
| } | |
| @override | |
| void stderr(String message) { | |
| io.stderr.writeln('${_createPrefix()}${ansi.red}$message${ansi.none}'); | |
| } | |
| @override | |
| void trace(String message) { | |
| io.stdout.writeln('${_createPrefix()}${ansi.gray}$message${ansi.none}'); | |
| } | |
| @override | |
| void write(String message) { | |
| io.stdout.write(message); | |
| } | |
| @override | |
| void writeCharCode(int charCode) { | |
| io.stdout.writeCharCode(charCode); | |
| } | |
| @override | |
| Progress progress(String message) => SimpleProgress(this, message); | |
| @override | |
| @Deprecated('This method will be removed in the future') | |
| void flush() {} | |
| String _createPrefix() { | |
| if (!logTime) { | |
| return ''; | |
| } | |
| var seconds = _timer.elapsedMilliseconds / 1000.0; | |
| final minutes = seconds ~/ 60; | |
| seconds -= minutes * 60.0; | |
| final buf = StringBuffer(); | |
| if (minutes > 0) { | |
| buf.write(minutes % 60); | |
| buf.write('m '); | |
| } | |
| buf.write(seconds.toStringAsFixed(3).padLeft(minutes > 0 ? 6 : 1, '0')); | |
| buf.write('s'); | |
| return '[${buf.toString().padLeft(11)}] '; | |
| } | |
| } |