blob: cd3a72e311ad09846e3960d793a0cf21884b8332 [file] [log] [blame]
// 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 =>
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}) {
return 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() {
var progress = _currentProgress;
if (progress != null) {
_currentProgress = null;
progress.cancel();
}
}
@override
Progress progress(String message) {
_cancelProgress();
var 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 = ['/', '-', '\\', '|'];
final Ansi ansi;
int _index = 0;
late final _timer = Timer.periodic(Duration(milliseconds: 80), (t) {
_index++;
_updateDisplay();
});
AnsiProgress(this.ansi, String message) : super(message) {
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[_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) {
var 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;
var minutes = seconds ~/ 60;
seconds -= minutes * 60.0;
var 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)}] ';
}
}