mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
459 lines
14 KiB
Dart
459 lines
14 KiB
Dart
// Copyright 2014 The Flutter 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 '../convert.dart';
|
|
import '../features.dart';
|
|
import 'io.dart' as io;
|
|
import 'logger.dart';
|
|
import 'platform.dart';
|
|
|
|
enum TerminalColor {
|
|
red,
|
|
green,
|
|
blue,
|
|
cyan,
|
|
yellow,
|
|
magenta,
|
|
grey,
|
|
}
|
|
|
|
/// A class that contains the context settings for command text output to the
|
|
/// console.
|
|
class OutputPreferences {
|
|
OutputPreferences({
|
|
bool? wrapText,
|
|
int? wrapColumn,
|
|
bool? showColor,
|
|
io.Stdio? stdio,
|
|
}) : _stdio = stdio,
|
|
wrapText = wrapText ?? stdio?.hasTerminal ?? false,
|
|
_overrideWrapColumn = wrapColumn,
|
|
showColor = showColor ?? false;
|
|
|
|
/// A version of this class for use in tests.
|
|
OutputPreferences.test({this.wrapText = false, int wrapColumn = kDefaultTerminalColumns, this.showColor = false})
|
|
: _overrideWrapColumn = wrapColumn, _stdio = null;
|
|
|
|
final io.Stdio? _stdio;
|
|
|
|
/// 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.
|
|
final bool wrapText;
|
|
|
|
/// The terminal width used by the [wrapText] function if there is no terminal
|
|
/// attached to [io.Stdio], --wrap is on, and --wrap-columns was not specified.
|
|
static const int kDefaultTerminalColumns = 100;
|
|
|
|
/// 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.
|
|
final int? _overrideWrapColumn;
|
|
int get wrapColumn {
|
|
return _overrideWrapColumn ?? _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]';
|
|
}
|
|
}
|
|
|
|
/// The command line terminal, if available.
|
|
// TODO(ianh): merge this with AnsiTerminal, the abstraction isn't giving us anything.
|
|
abstract class Terminal {
|
|
/// Create a new test [Terminal].
|
|
///
|
|
/// If not specified, [supportsColor] defaults to `false`.
|
|
factory Terminal.test({bool supportsColor, bool supportsEmoji}) = _TestTerminal;
|
|
|
|
/// Whether the current terminal supports color escape codes.
|
|
///
|
|
/// Check [isCliAnimationEnabled] as well before using `\r` or ANSI sequences
|
|
/// to perform animations.
|
|
bool get supportsColor;
|
|
|
|
/// Whether animations should be used in the output.
|
|
bool get isCliAnimationEnabled;
|
|
|
|
/// Configures isCliAnimationEnabled based on a [FeatureFlags] object.
|
|
void applyFeatureFlags(FeatureFlags flags);
|
|
|
|
/// Whether the current terminal can display emoji.
|
|
bool get supportsEmoji;
|
|
|
|
/// When we have a choice of styles (e.g. animated spinners), this selects the
|
|
/// style to use.
|
|
int get preferredStyle;
|
|
|
|
/// Whether we are interacting with the flutter tool via the terminal.
|
|
///
|
|
/// If not set, defaults to false.
|
|
bool get usesTerminalUi;
|
|
set usesTerminalUi(bool value);
|
|
|
|
/// Whether there is a terminal attached to stdin.
|
|
///
|
|
/// If true, this usually indicates that a user is using the CLI as
|
|
/// opposed to using an IDE. This can be used to determine
|
|
/// whether it is appropriate to show a terminal prompt,
|
|
/// or whether an automatic selection should be made instead.
|
|
bool get stdinHasTerminal;
|
|
|
|
/// Warning mark to use in stdout or stderr.
|
|
String get warningMark;
|
|
|
|
/// Success mark to use in stdout.
|
|
String get successMark;
|
|
|
|
String bolden(String message);
|
|
|
|
String color(String message, TerminalColor color);
|
|
|
|
String clearScreen();
|
|
|
|
bool get singleCharMode;
|
|
set singleCharMode(bool value);
|
|
|
|
/// Return keystrokes from the console.
|
|
///
|
|
/// This is a single-subscription stream. This stream may be closed before
|
|
/// the application exits.
|
|
///
|
|
/// Useful when the console is in [singleCharMode].
|
|
Stream<String> get keystrokes;
|
|
|
|
/// 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`.
|
|
///
|
|
/// The accepted characters must be a String with a length of 1, excluding any
|
|
/// whitespace characters such as `\t`, `\n`, or ` `.
|
|
///
|
|
/// If [usesTerminalUi] is false, throws a [StateError].
|
|
Future<String> promptForCharInput(
|
|
List<String> acceptedCharacters, {
|
|
required Logger logger,
|
|
String? prompt,
|
|
int? defaultChoiceIndex,
|
|
bool displayAcceptedCharacters = true,
|
|
});
|
|
}
|
|
|
|
class AnsiTerminal implements Terminal {
|
|
AnsiTerminal({
|
|
required io.Stdio stdio,
|
|
required Platform platform,
|
|
DateTime? now, // Time used to determine preferredStyle. Defaults to 0001-01-01 00:00.
|
|
bool defaultCliAnimationEnabled = true,
|
|
})
|
|
: _stdio = stdio,
|
|
_platform = platform,
|
|
_now = now ?? DateTime(1),
|
|
_isCliAnimationEnabled = defaultCliAnimationEnabled;
|
|
|
|
final io.Stdio _stdio;
|
|
final Platform _platform;
|
|
final DateTime _now;
|
|
|
|
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[90m';
|
|
|
|
// Moves cursor up 1 line.
|
|
static const String cursorUpLineCode = '\u001b[1A';
|
|
|
|
// Moves cursor to the beginning of the line.
|
|
static const String cursorBeginningOfLineCode = '\u001b[1G';
|
|
|
|
// Clear the entire line, cursor position does not change.
|
|
static const String clearEntireLineCode = '\u001b[2K';
|
|
|
|
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]!;
|
|
|
|
@override
|
|
bool get supportsColor => _platform.stdoutSupportsAnsi;
|
|
|
|
@override
|
|
bool get isCliAnimationEnabled => _isCliAnimationEnabled;
|
|
|
|
bool _isCliAnimationEnabled;
|
|
|
|
@override
|
|
void applyFeatureFlags(FeatureFlags flags) {
|
|
_isCliAnimationEnabled = flags.isCliAnimationEnabled;
|
|
}
|
|
|
|
// Assume unicode emojis are supported when not on Windows.
|
|
// If we are on Windows, unicode emojis are supported in Windows Terminal,
|
|
// which sets the WT_SESSION environment variable. See:
|
|
// https://github.com/microsoft/terminal/blob/master/doc/user-docs/index.md#tips-and-tricks
|
|
@override
|
|
bool get supportsEmoji => !_platform.isWindows
|
|
|| _platform.environment.containsKey('WT_SESSION');
|
|
|
|
@override
|
|
int get preferredStyle {
|
|
const int workdays = DateTime.friday;
|
|
if (_now.weekday <= workdays) {
|
|
return _now.weekday - 1;
|
|
}
|
|
return _now.hour + workdays;
|
|
}
|
|
|
|
final RegExp _boldControls = RegExp(
|
|
'(${RegExp.escape(resetBold)}|${RegExp.escape(bold)})',
|
|
);
|
|
|
|
@override
|
|
bool usesTerminalUi = false;
|
|
|
|
@override
|
|
String get warningMark {
|
|
return bolden(color('[!]', TerminalColor.red));
|
|
}
|
|
|
|
@override
|
|
String get successMark {
|
|
return bolden(color('✓', TerminalColor.green));
|
|
}
|
|
|
|
@override
|
|
String bolden(String message) {
|
|
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;
|
|
}
|
|
|
|
@override
|
|
String color(String message, TerminalColor color) {
|
|
if (!supportsColor || 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;
|
|
}
|
|
|
|
@override
|
|
String clearScreen() => supportsColor && isCliAnimationEnabled ? clear : '\n\n';
|
|
|
|
/// Returns ANSI codes to clear [numberOfLines] lines starting with the line
|
|
/// the cursor is on.
|
|
///
|
|
/// If the terminal does not support ANSI codes, returns an empty string.
|
|
String clearLines(int numberOfLines) {
|
|
if (!supportsColor || !isCliAnimationEnabled) {
|
|
return '';
|
|
}
|
|
return cursorBeginningOfLineCode +
|
|
clearEntireLineCode +
|
|
(cursorUpLineCode + clearEntireLineCode) * (numberOfLines - 1);
|
|
}
|
|
|
|
@override
|
|
bool get singleCharMode {
|
|
if (!_stdio.stdinHasTerminal) {
|
|
return false;
|
|
}
|
|
final io.Stdin stdin = _stdio.stdin as io.Stdin;
|
|
return stdin.lineMode && stdin.echoMode;
|
|
}
|
|
@override
|
|
set singleCharMode(bool value) {
|
|
if (!_stdio.stdinHasTerminal) {
|
|
return;
|
|
}
|
|
final io.Stdin stdin = _stdio.stdin as io.Stdin;
|
|
|
|
try {
|
|
// 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;
|
|
}
|
|
} on io.StdinException {
|
|
// If the pipe to STDIN has been closed it's probably because the
|
|
// terminal has been closed, and there is nothing actionable to do here.
|
|
}
|
|
}
|
|
|
|
@override
|
|
bool get stdinHasTerminal => _stdio.stdinHasTerminal;
|
|
|
|
Stream<String>? _broadcastStdInString;
|
|
|
|
@override
|
|
Stream<String> get keystrokes {
|
|
return _broadcastStdInString ??= _stdio.stdin.transform<String>(const AsciiDecoder(allowInvalid: true)).asBroadcastStream();
|
|
}
|
|
|
|
@override
|
|
Future<String> promptForCharInput(
|
|
List<String> acceptedCharacters, {
|
|
required Logger logger,
|
|
String? prompt,
|
|
int? defaultChoiceIndex,
|
|
bool displayAcceptedCharacters = true,
|
|
}) async {
|
|
assert(acceptedCharacters.isNotEmpty);
|
|
assert(prompt == null || prompt.isNotEmpty);
|
|
if (!usesTerminalUi) {
|
|
throw StateError('cannot prompt without a terminal ui');
|
|
}
|
|
List<String> charactersToDisplay = acceptedCharacters;
|
|
if (defaultChoiceIndex != null) {
|
|
assert(defaultChoiceIndex >= 0 && defaultChoiceIndex < acceptedCharacters.length);
|
|
charactersToDisplay = List<String>.of(charactersToDisplay);
|
|
charactersToDisplay[defaultChoiceIndex] = bolden(charactersToDisplay[defaultChoiceIndex]);
|
|
acceptedCharacters.add('');
|
|
}
|
|
String? choice;
|
|
singleCharMode = true;
|
|
while (choice == null || choice.length > 1 || !acceptedCharacters.contains(choice)) {
|
|
if (prompt != null) {
|
|
logger.printStatus(prompt, emphasis: true, newline: false);
|
|
if (displayAcceptedCharacters) {
|
|
logger.printStatus(' [${charactersToDisplay.join("|")}]', newline: false);
|
|
}
|
|
// prompt ends with ': '
|
|
logger.printStatus(': ', emphasis: true, newline: false);
|
|
}
|
|
choice = (await keystrokes.first).trim();
|
|
logger.printStatus(choice);
|
|
}
|
|
singleCharMode = false;
|
|
if (defaultChoiceIndex != null && choice == '') {
|
|
choice = acceptedCharacters[defaultChoiceIndex];
|
|
}
|
|
return choice;
|
|
}
|
|
}
|
|
|
|
class _TestTerminal implements Terminal {
|
|
_TestTerminal({this.supportsColor = false, this.supportsEmoji = false});
|
|
|
|
@override
|
|
bool usesTerminalUi = false;
|
|
|
|
@override
|
|
String bolden(String message) => message;
|
|
|
|
@override
|
|
String clearScreen() => '\n\n';
|
|
|
|
@override
|
|
String color(String message, TerminalColor color) => message;
|
|
|
|
@override
|
|
Stream<String> get keystrokes => const Stream<String>.empty();
|
|
|
|
@override
|
|
Future<String> promptForCharInput(List<String> acceptedCharacters, {
|
|
required Logger logger,
|
|
String? prompt,
|
|
int? defaultChoiceIndex,
|
|
bool displayAcceptedCharacters = true,
|
|
}) {
|
|
throw UnsupportedError('promptForCharInput not supported in the test terminal.');
|
|
}
|
|
|
|
@override
|
|
bool get singleCharMode => false;
|
|
@override
|
|
set singleCharMode(bool value) { }
|
|
|
|
@override
|
|
final bool supportsColor;
|
|
|
|
@override
|
|
bool get isCliAnimationEnabled => supportsColor && _isCliAnimationEnabled;
|
|
|
|
bool _isCliAnimationEnabled = true;
|
|
|
|
@override
|
|
void applyFeatureFlags(FeatureFlags flags) {
|
|
_isCliAnimationEnabled = flags.isCliAnimationEnabled;
|
|
}
|
|
|
|
@override
|
|
final bool supportsEmoji;
|
|
|
|
@override
|
|
int get preferredStyle => 0;
|
|
|
|
@override
|
|
bool get stdinHasTerminal => false;
|
|
|
|
@override
|
|
String get successMark => '✓';
|
|
|
|
@override
|
|
String get warningMark => '[!]';
|
|
}
|