-
Notifications
You must be signed in to change notification settings - Fork 26.9k
/
terminal.dart
385 lines (326 loc) · 12 KB
/
terminal.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
// 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 '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.
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.
bool get supportsColor;
/// 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();
set singleCharMode(bool value);
/// Return keystrokes from the console.
///
/// 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.
})
: _stdio = stdio,
_platform = platform,
_now = now ?? DateTime(1);
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';
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;
// 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) {
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;
}
@override
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;
}
@override
String clearScreen() => supportsColor ? clear : '\n\n';
@override
set singleCharMode(bool value) {
if (!_stdio.stdinHasTerminal) {
return;
}
final io.Stdin stdin = _stdio.stdin as io.Stdin;
// 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;
}
}
@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
set singleCharMode(bool value) { }
@override
final bool supportsColor;
@override
final bool supportsEmoji;
@override
int get preferredStyle => 0;
@override
bool get stdinHasTerminal => false;
@override
String get successMark => '✓';
@override
String get warningMark => '[!]';
}