diff --git a/CHANGELOG.md b/CHANGELOG.md index e4cd8cd..7d05c61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ ## unreleased - Use the new `Platform.resolvedExecutable` API to locate the SDK +- add the `cli_logging.dart` library - some utilities to help cli tools + display output ## 0.0.1+3 diff --git a/README.md b/README.md index bf2dcf9..e27d663 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ interact with the Dart SDK (such as the [analyzer][analyzer]). [![Build Status](https://travis-ci.org/dart-lang/cli_util.svg)](https://travis-ci.org/dart-lang/cli_util) -## Usage +## Locating the Dart SDK ```dart import 'dart:io'; @@ -26,6 +26,38 @@ main(args) { } ``` +## Displaying output and progress + +`package:cli_util` can also be used to help CLI tools display output and progress. +It has a logging mechanism which can help differentiate between regular tool +output and error messages, and can facilitate having a more verbose (`-v`) mode for +output. + +In addition, it can display an indeterminate progress spinner for longer running +tasks, and optionally display the elapsed time when finished: + +```dart +import 'package:cli_util/cli_logging.dart'; + +main(List args) async { + bool verbose = args.contains('-v'); + Logger logger = verbose ? new Logger.verbose() : new Logger.standard(); + + logger.stdout('Hello world!'); + logger.trace('message 1'); + await new Future.delayed(new Duration(milliseconds: 200)); + logger.trace('message 2'); + logger.trace('message 3'); + + Progress progress = logger.progress('doing some work'); + await new Future.delayed(new Duration(seconds: 2)); + progress.finish(showTiming: true); + + logger.stdout('All ${logger.ansi.emphasized('done')}.'); + logger.flush(); +} +``` + ## Features and bugs Please file feature requests and bugs at the [issue tracker][tracker]. diff --git a/example/main.dart b/example/main.dart new file mode 100644 index 0000000..0cc6846 --- /dev/null +++ b/example/main.dart @@ -0,0 +1,25 @@ +// 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. + +import 'dart:async'; + +import 'package:cli_util/cli_logging.dart'; + +main(List args) async { + bool verbose = args.contains('-v'); + Logger logger = verbose ? new Logger.verbose() : new Logger.standard(); + + logger.stdout('Hello world!'); + logger.trace('message 1'); + await new Future.delayed(new Duration(milliseconds: 200)); + logger.trace('message 2'); + logger.trace('message 3'); + + Progress progress = logger.progress('doing some work'); + await new Future.delayed(new Duration(seconds: 2)); + progress.finish(showTiming: true); + + logger.stdout('All ${logger.ansi.emphasized('done')}.'); + logger.flush(); +} diff --git a/lib/cli_logging.dart b/lib/cli_logging.dart new file mode 100644 index 0000000..72ec89a --- /dev/null +++ b/lib/cli_logging.dart @@ -0,0 +1,266 @@ +// 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. + +import 'dart:async'; +import 'dart:io' as io; + +/// create aesthetic output. + +/// 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; + + /// 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); + } + + 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 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); + } else if (showTiming) { + String time = (elapsed.inMilliseconds / 1000.0).toStringAsFixed(1); + io.stdout.write('${time}s'); + } + 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(); + } + + 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]'; + } +} diff --git a/lib/cli_util.dart b/lib/cli_util.dart index 62191ec..2c8faa2 100644 --- a/lib/cli_util.dart +++ b/lib/cli_util.dart @@ -2,7 +2,7 @@ // 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. -library cli_util; +/// Utilities to return the Dart SDK location. import 'dart:io';