• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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