1// Copyright 2017 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5import 'dart:async'; 6 7import '../convert.dart'; 8import '../globals.dart'; 9import 'context.dart'; 10import 'io.dart' as io; 11import 'platform.dart'; 12import 'utils.dart'; 13 14final AnsiTerminal _kAnsiTerminal = AnsiTerminal(); 15 16AnsiTerminal get terminal { 17 return (context == null || context.get<AnsiTerminal>() == null) 18 ? _kAnsiTerminal 19 : context.get<AnsiTerminal>(); 20} 21 22enum TerminalColor { 23 red, 24 green, 25 blue, 26 cyan, 27 yellow, 28 magenta, 29 grey, 30} 31 32final OutputPreferences _kOutputPreferences = OutputPreferences(); 33 34OutputPreferences get outputPreferences => (context == null || context.get<OutputPreferences>() == null) 35 ? _kOutputPreferences 36 : context.get<OutputPreferences>(); 37 38/// A class that contains the context settings for command text output to the 39/// console. 40class OutputPreferences { 41 OutputPreferences({ 42 bool wrapText, 43 int wrapColumn, 44 bool showColor, 45 }) : wrapText = wrapText ?? io.stdio?.hasTerminal ?? const io.Stdio().hasTerminal, 46 _overrideWrapColumn = wrapColumn, 47 showColor = showColor ?? platform.stdoutSupportsAnsi ?? false; 48 49 /// If [wrapText] is true, then any text sent to the context's [Logger] 50 /// instance (e.g. from the [printError] or [printStatus] functions) will be 51 /// wrapped (newlines added between words) to be no longer than the 52 /// [wrapColumn] specifies. Defaults to true if there is a terminal. To 53 /// determine if there's a terminal, [OutputPreferences] asks the context's 54 /// stdio to see, and if that's not set, it tries creating a new [io.Stdio] 55 /// and asks it if there is a terminal. 56 final bool wrapText; 57 58 /// The column at which output sent to the context's [Logger] instance 59 /// (e.g. from the [printError] or [printStatus] functions) will be wrapped. 60 /// Ignored if [wrapText] is false. Defaults to the width of the output 61 /// terminal, or to [kDefaultTerminalColumns] if not writing to a terminal. 62 /// To find out if we're writing to a terminal, it tries the context's stdio, 63 /// and if that's not set, it tries creating a new [io.Stdio] and asks it, if 64 /// that doesn't have an idea of the terminal width, then we just use a 65 /// default of 100. It will be ignored if [wrapText] is false. 66 final int _overrideWrapColumn; 67 int get wrapColumn { 68 return _overrideWrapColumn ?? io.stdio?.terminalColumns 69 ?? const io.Stdio().terminalColumns ?? kDefaultTerminalColumns; 70 } 71 72 /// Whether or not to output ANSI color codes when writing to the output 73 /// terminal. Defaults to whatever [platform.stdoutSupportsAnsi] says if 74 /// writing to a terminal, and false otherwise. 75 final bool showColor; 76 77 @override 78 String toString() { 79 return '$runtimeType[wrapText: $wrapText, wrapColumn: $wrapColumn, showColor: $showColor]'; 80 } 81} 82 83class AnsiTerminal { 84 static const String bold = '\u001B[1m'; 85 static const String resetAll = '\u001B[0m'; 86 static const String resetColor = '\u001B[39m'; 87 static const String resetBold = '\u001B[22m'; 88 static const String clear = '\u001B[2J\u001B[H'; 89 90 static const String red = '\u001b[31m'; 91 static const String green = '\u001b[32m'; 92 static const String blue = '\u001b[34m'; 93 static const String cyan = '\u001b[36m'; 94 static const String magenta = '\u001b[35m'; 95 static const String yellow = '\u001b[33m'; 96 static const String grey = '\u001b[1;30m'; 97 98 static const Map<TerminalColor, String> _colorMap = <TerminalColor, String>{ 99 TerminalColor.red: red, 100 TerminalColor.green: green, 101 TerminalColor.blue: blue, 102 TerminalColor.cyan: cyan, 103 TerminalColor.magenta: magenta, 104 TerminalColor.yellow: yellow, 105 TerminalColor.grey: grey, 106 }; 107 108 static String colorCode(TerminalColor color) => _colorMap[color]; 109 110 bool get supportsColor => platform.stdoutSupportsAnsi ?? false; 111 final RegExp _boldControls = RegExp('(${RegExp.escape(resetBold)}|${RegExp.escape(bold)})'); 112 113 String bolden(String message) { 114 assert(message != null); 115 if (!supportsColor || message.isEmpty) 116 return message; 117 final StringBuffer buffer = StringBuffer(); 118 for (String line in message.split('\n')) { 119 // If there were bolds or resetBolds in the string before, then nuke them: 120 // they're redundant. This prevents previously embedded resets from 121 // stopping the boldness. 122 line = line.replaceAll(_boldControls, ''); 123 buffer.writeln('$bold$line$resetBold'); 124 } 125 final String result = buffer.toString(); 126 // avoid introducing a new newline to the emboldened text 127 return (!message.endsWith('\n') && result.endsWith('\n')) 128 ? result.substring(0, result.length - 1) 129 : result; 130 } 131 132 String color(String message, TerminalColor color) { 133 assert(message != null); 134 if (!supportsColor || color == null || message.isEmpty) 135 return message; 136 final StringBuffer buffer = StringBuffer(); 137 final String colorCodes = _colorMap[color]; 138 for (String line in message.split('\n')) { 139 // If there were resets in the string before, then keep them, but 140 // restart the color right after. This prevents embedded resets from 141 // stopping the colors, and allows nesting of colors. 142 line = line.replaceAll(resetColor, '$resetColor$colorCodes'); 143 buffer.writeln('$colorCodes$line$resetColor'); 144 } 145 final String result = buffer.toString(); 146 // avoid introducing a new newline to the colored text 147 return (!message.endsWith('\n') && result.endsWith('\n')) 148 ? result.substring(0, result.length - 1) 149 : result; 150 } 151 152 String clearScreen() => supportsColor ? clear : '\n\n'; 153 154 set singleCharMode(bool value) { 155 final Stream<List<int>> stdin = io.stdin; 156 if (stdin is io.Stdin && stdin.hasTerminal) { 157 // The order of setting lineMode and echoMode is important on Windows. 158 if (value) { 159 stdin.echoMode = false; 160 stdin.lineMode = false; 161 } else { 162 stdin.lineMode = true; 163 stdin.echoMode = true; 164 } 165 } 166 } 167 168 Stream<String> _broadcastStdInString; 169 170 /// Return keystrokes from the console. 171 /// 172 /// Useful when the console is in [singleCharMode]. 173 Stream<String> get keystrokes { 174 _broadcastStdInString ??= io.stdin.transform<String>(const AsciiDecoder(allowInvalid: true)).asBroadcastStream(); 175 return _broadcastStdInString; 176 } 177 178 /// Prompts the user to input a character within a given list. Re-prompts if 179 /// entered character is not in the list. 180 /// 181 /// The `prompt`, if non-null, is the text displayed prior to waiting for user 182 /// input each time. If `prompt` is non-null and `displayAcceptedCharacters` 183 /// is true, the accepted keys are printed next to the `prompt`. 184 /// 185 /// The returned value is the user's input; if `defaultChoiceIndex` is not 186 /// null, and the user presses enter without any other input, the return value 187 /// will be the character in `acceptedCharacters` at the index given by 188 /// `defaultChoiceIndex`. 189 Future<String> promptForCharInput( 190 List<String> acceptedCharacters, { 191 String prompt, 192 int defaultChoiceIndex, 193 bool displayAcceptedCharacters = true, 194 }) async { 195 assert(acceptedCharacters != null); 196 assert(acceptedCharacters.isNotEmpty); 197 assert(prompt == null || prompt.isNotEmpty); 198 assert(displayAcceptedCharacters != null); 199 List<String> charactersToDisplay = acceptedCharacters; 200 if (defaultChoiceIndex != null) { 201 assert(defaultChoiceIndex >= 0 && defaultChoiceIndex < acceptedCharacters.length); 202 charactersToDisplay = List<String>.from(charactersToDisplay); 203 charactersToDisplay[defaultChoiceIndex] = bolden(charactersToDisplay[defaultChoiceIndex]); 204 acceptedCharacters.add('\n'); 205 } 206 String choice; 207 singleCharMode = true; 208 while (choice == null || choice.length > 1 || !acceptedCharacters.contains(choice)) { 209 if (prompt != null) { 210 printStatus(prompt, emphasis: true, newline: false); 211 if (displayAcceptedCharacters) 212 printStatus(' [${charactersToDisplay.join("|")}]', newline: false); 213 printStatus(': ', emphasis: true, newline: false); 214 } 215 choice = await keystrokes.first; 216 printStatus(choice); 217 } 218 singleCharMode = false; 219 if (defaultChoiceIndex != null && choice == '\n') 220 choice = acceptedCharacters[defaultChoiceIndex]; 221 return choice; 222 } 223} 224