blob: 396b9a22b8652860b8114ac66c64308365f3573d [file] [log] [blame]
// Copyright 2017 The Chromium Authors. 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 '../convert.dart';
import '../globals.dart';
import 'context.dart';
import 'io.dart' as io;
import 'platform.dart';
import 'utils.dart';
final AnsiTerminal _kAnsiTerminal = AnsiTerminal();
AnsiTerminal get terminal {
return (context == null || context.get<AnsiTerminal>() == null)
? _kAnsiTerminal
: context.get<AnsiTerminal>();
}
enum TerminalColor {
red,
green,
blue,
cyan,
yellow,
magenta,
grey,
}
final OutputPreferences _kOutputPreferences = OutputPreferences();
OutputPreferences get outputPreferences => (context == null || context.get<OutputPreferences>() == null)
? _kOutputPreferences
: context.get<OutputPreferences>();
/// A class that contains the context settings for command text output to the
/// console.
class OutputPreferences {
OutputPreferences({
bool wrapText,
int wrapColumn,
bool showColor,
}) : wrapText = wrapText ?? io.stdio?.hasTerminal ?? const io.Stdio().hasTerminal,
_overrideWrapColumn = wrapColumn,
showColor = showColor ?? platform.stdoutSupportsAnsi ?? false;
/// If [wrapText] is true, then any text sent to the context's [Logger]
/// instance (e.g. from the [printError] or [printStatus] functions) will be
/// wrapped (newlines added between words) to be no longer than the
/// [wrapColumn] specifies. Defaults to true if there is a terminal. To
/// determine if there's a terminal, [OutputPreferences] asks the context's
/// stdio to see, and if that's not set, it tries creating a new [io.Stdio]
/// and asks it if there is a terminal.
final bool wrapText;
/// The column at which output sent to the context's [Logger] instance
/// (e.g. from the [printError] or [printStatus] functions) will be wrapped.
/// Ignored if [wrapText] is false. Defaults to the width of the output
/// terminal, or to [kDefaultTerminalColumns] if not writing to a terminal.
/// To find out if we're writing to a terminal, it tries the context's stdio,
/// and if that's not set, it tries creating a new [io.Stdio] and asks it, if
/// that doesn't have an idea of the terminal width, then we just use a
/// default of 100. It will be ignored if [wrapText] is false.
final int _overrideWrapColumn;
int get wrapColumn {
return _overrideWrapColumn ?? io.stdio?.terminalColumns
?? const io.Stdio().terminalColumns ?? kDefaultTerminalColumns;
}
/// Whether or not to output ANSI color codes when writing to the output
/// terminal. Defaults to whatever [platform.stdoutSupportsAnsi] says if
/// writing to a terminal, and false otherwise.
final bool showColor;
@override
String toString() {
return '$runtimeType[wrapText: $wrapText, wrapColumn: $wrapColumn, showColor: $showColor]';
}
}
class AnsiTerminal {
static const String bold = '\u001B[1m';
static const String resetAll = '\u001B[0m';
static const String resetColor = '\u001B[39m';
static const String resetBold = '\u001B[22m';
static const String clear = '\u001B[2J\u001B[H';
static const String red = '\u001b[31m';
static const String green = '\u001b[32m';
static const String blue = '\u001b[34m';
static const String cyan = '\u001b[36m';
static const String magenta = '\u001b[35m';
static const String yellow = '\u001b[33m';
static const String grey = '\u001b[1;30m';
static const Map<TerminalColor, String> _colorMap = <TerminalColor, String>{
TerminalColor.red: red,
TerminalColor.green: green,
TerminalColor.blue: blue,
TerminalColor.cyan: cyan,
TerminalColor.magenta: magenta,
TerminalColor.yellow: yellow,
TerminalColor.grey: grey,
};
static String colorCode(TerminalColor color) => _colorMap[color];
bool get supportsColor => platform.stdoutSupportsAnsi ?? false;
final RegExp _boldControls = RegExp('(${RegExp.escape(resetBold)}|${RegExp.escape(bold)})');
String bolden(String message) {
assert(message != null);
if (!supportsColor || message.isEmpty)
return message;
final StringBuffer buffer = StringBuffer();
for (String line in message.split('\n')) {
// If there were bolds or resetBolds in the string before, then nuke them:
// they're redundant. This prevents previously embedded resets from
// stopping the boldness.
line = line.replaceAll(_boldControls, '');
buffer.writeln('$bold$line$resetBold');
}
final String result = buffer.toString();
// avoid introducing a new newline to the emboldened text
return (!message.endsWith('\n') && result.endsWith('\n'))
? result.substring(0, result.length - 1)
: result;
}
String color(String message, TerminalColor color) {
assert(message != null);
if (!supportsColor || color == null || message.isEmpty)
return message;
final StringBuffer buffer = StringBuffer();
final String colorCodes = _colorMap[color];
for (String line in message.split('\n')) {
// If there were resets in the string before, then keep them, but
// restart the color right after. This prevents embedded resets from
// stopping the colors, and allows nesting of colors.
line = line.replaceAll(resetColor, '$resetColor$colorCodes');
buffer.writeln('$colorCodes$line$resetColor');
}
final String result = buffer.toString();
// avoid introducing a new newline to the colored text
return (!message.endsWith('\n') && result.endsWith('\n'))
? result.substring(0, result.length - 1)
: result;
}
String clearScreen() => supportsColor ? clear : '\n\n';
set singleCharMode(bool value) {
final Stream<List<int>> stdin = io.stdin;
if (stdin is io.Stdin && stdin.hasTerminal) {
// The order of setting lineMode and echoMode is important on Windows.
if (value) {
stdin.echoMode = false;
stdin.lineMode = false;
} else {
stdin.lineMode = true;
stdin.echoMode = true;
}
}
}
Stream<String> _broadcastStdInString;
/// Return keystrokes from the console.
///
/// Useful when the console is in [singleCharMode].
Stream<String> get keystrokes {
_broadcastStdInString ??= io.stdin.transform<String>(const AsciiDecoder(allowInvalid: true)).asBroadcastStream();
return _broadcastStdInString;
}
/// Prompts the user to input a character within a given list. Re-prompts if
/// entered character is not in the list.
///
/// The `prompt`, if non-null, is the text displayed prior to waiting for user
/// input each time. If `prompt` is non-null and `displayAcceptedCharacters`
/// is true, the accepted keys are printed next to the `prompt`.
///
/// The returned value is the user's input; if `defaultChoiceIndex` is not
/// null, and the user presses enter without any other input, the return value
/// will be the character in `acceptedCharacters` at the index given by
/// `defaultChoiceIndex`.
Future<String> promptForCharInput(
List<String> acceptedCharacters, {
String prompt,
int defaultChoiceIndex,
bool displayAcceptedCharacters = true,
}) async {
assert(acceptedCharacters != null);
assert(acceptedCharacters.isNotEmpty);
assert(prompt == null || prompt.isNotEmpty);
assert(displayAcceptedCharacters != null);
List<String> charactersToDisplay = acceptedCharacters;
if (defaultChoiceIndex != null) {
assert(defaultChoiceIndex >= 0 && defaultChoiceIndex < acceptedCharacters.length);
charactersToDisplay = List<String>.from(charactersToDisplay);
charactersToDisplay[defaultChoiceIndex] = bolden(charactersToDisplay[defaultChoiceIndex]);
acceptedCharacters.add('\n');
}
String choice;
singleCharMode = true;
while (choice == null || choice.length > 1 || !acceptedCharacters.contains(choice)) {
if (prompt != null) {
printStatus(prompt, emphasis: true, newline: false);
if (displayAcceptedCharacters)
printStatus(' [${charactersToDisplay.join("|")}]', newline: false);
printStatus(': ', emphasis: true, newline: false);
}
choice = await keystrokes.first;
printStatus(choice);
}
singleCharMode = false;
if (defaultChoiceIndex != null && choice == '\n')
choice = acceptedCharacters[defaultChoiceIndex];
return choice;
}
}