// 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 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)}] '; } }