| // 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 'dart:convert' show ASCII; |
| |
| import 'package:quiver/strings.dart'; |
| |
| import '../globals.dart'; |
| import 'context.dart'; |
| import 'io.dart' as io; |
| import 'platform.dart'; |
| |
| final AnsiTerminal _kAnsiTerminal = new AnsiTerminal(); |
| |
| AnsiTerminal get terminal { |
| return context == null |
| ? _kAnsiTerminal |
| : context[AnsiTerminal]; |
| } |
| |
| class AnsiTerminal { |
| static const String _bold = '\u001B[1m'; |
| static const String _reset = '\u001B[0m'; |
| static const String _clear = '\u001B[2J\u001B[H'; |
| |
| static const int _ENXIO = 6; |
| static const int _ENOTTY = 25; |
| static const int _ENETRESET = 102; |
| static const int _INVALID_HANDLE = 6; |
| |
| /// Setting the line mode can throw for some terminals (with "Operation not |
| /// supported on socket"), but the error can be safely ignored. |
| static const List<int> _lineModeIgnorableErrors = const <int>[ |
| _ENXIO, |
| _ENOTTY, |
| _ENETRESET, |
| _INVALID_HANDLE, |
| ]; |
| |
| bool supportsColor = platform.stdoutSupportsAnsi; |
| |
| String bolden(String message) { |
| if (!supportsColor) |
| return message; |
| final StringBuffer buffer = new StringBuffer(); |
| for (String line in message.split('\n')) |
| buffer.writeln('$_bold$line$_reset'); |
| 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 clearScreen() => supportsColor ? _clear : '\n\n'; |
| |
| set singleCharMode(bool value) { |
| // TODO(goderbauer): instead of trying to set lineMode and then catching |
| // [_ENOTTY] or [_INVALID_HANDLE], we should check beforehand if stdin is |
| // connected to a terminal or not. |
| // (Requires https://github.com/dart-lang/sdk/issues/29083 to be resolved.) |
| final Stream<List<int>> stdin = io.stdin; |
| if (stdin is 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 catch (error) { |
| if (!_lineModeIgnorableErrors.contains(error.osError?.errorCode)) |
| rethrow; |
| } |
| } |
| } |
| |
| Stream<String> _broadcastStdInString; |
| |
| /// Return keystrokes from the console. |
| /// |
| /// Useful when the console is in [singleCharMode]. |
| Stream<String> get onCharInput { |
| _broadcastStdInString ??= io.stdin.transform(ASCII.decoder).asBroadcastStream(); |
| return _broadcastStdInString; |
| } |
| |
| /// Prompts the user to input a chraracter within the accepted list. |
| /// Reprompts if inputted character is not in the list. |
| /// |
| /// `prompt` is the text displayed prior to waiting for user input each time. |
| /// `defaultChoiceIndex`, if given, will be the character in `acceptedCharacters` |
| /// in the index given if the user presses enter without any key input. |
| /// `displayAcceptedCharacters` prints also the accepted keys next to the `prompt` if true. |
| /// |
| /// Throws a [TimeoutException] if a `timeout` is provided and its duration |
| /// expired without user input. Duration resets per key press. |
| Future<String> promptForCharInput( |
| List<String> acceptedCharacters, { |
| String prompt, |
| int defaultChoiceIndex, |
| bool displayAcceptedCharacters: true, |
| Duration timeout, |
| }) async { |
| assert(acceptedCharacters != null); |
| assert(acceptedCharacters.isNotEmpty); |
| List<String> charactersToDisplay = acceptedCharacters; |
| if (defaultChoiceIndex != null) { |
| assert(defaultChoiceIndex >= 0 && defaultChoiceIndex < acceptedCharacters.length); |
| charactersToDisplay = new List<String>.from(charactersToDisplay); |
| charactersToDisplay[defaultChoiceIndex] = bolden(charactersToDisplay[defaultChoiceIndex]); |
| acceptedCharacters.add('\n'); |
| } |
| String choice; |
| singleCharMode = true; |
| while (isEmpty(choice) || choice.length != 1 || !acceptedCharacters.contains(choice)) { |
| if (isNotEmpty(prompt)) { |
| printStatus(prompt, emphasis: true, newline: false); |
| if (displayAcceptedCharacters) |
| printStatus(' [${charactersToDisplay.join("|")}]', newline: false); |
| printStatus(': ', emphasis: true, newline: false); |
| } |
| Future<String> inputFuture = onCharInput.first; |
| if (timeout != null) |
| inputFuture = inputFuture.timeout(timeout); |
| choice = await inputFuture; |
| printStatus(choice); |
| } |
| singleCharMode = false; |
| if (defaultChoiceIndex != null && choice == '\n') |
| choice = acceptedCharacters[defaultChoiceIndex]; |
| return choice; |
| } |
| } |
| |