blob: c7693eb57ad750f38c54f38e9378d71fec8d4ed5 [file] [log] [blame]
Tom Rini10e47792018-05-06 17:58:06 -04001# SPDX-License-Identifier: GPL-2.0+
Simon Glass26132882012-01-14 15:12:45 +00002# Copyright (c) 2011 The Chromium OS Authors.
3#
Simon Glass26132882012-01-14 15:12:45 +00004
5"""Terminal utilities
6
7This module handles terminal interaction including ANSI color codes.
8"""
9
Paul Burtonc3931342016-09-27 16:03:50 +010010from __future__ import print_function
11
Simon Glassa9f7edb2012-12-15 10:42:01 +000012import os
Simon Glass5f9325d2020-04-09 15:08:40 -060013import re
Simon Glassa9f7edb2012-12-15 10:42:01 +000014import sys
15
16# Selection of when we want our output to be colored
17COLOR_IF_TERMINAL, COLOR_ALWAYS, COLOR_NEVER = range(3)
18
Simon Glassfb35f9f2014-09-05 19:00:06 -060019# Initially, we are set up to print to the terminal
20print_test_mode = False
21print_test_list = []
22
Simon Glass5f9325d2020-04-09 15:08:40 -060023# The length of the last line printed without a newline
24last_print_len = None
25
26# credit:
27# stackoverflow.com/questions/14693701/how-can-i-remove-the-ansi-escape-sequences-from-a-string-in-python
28ansi_escape = re.compile(r'\x1b(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
29
Simon Glassfb35f9f2014-09-05 19:00:06 -060030class PrintLine:
31 """A line of text output
32
33 Members:
34 text: Text line that was printed
35 newline: True to output a newline after the text
36 colour: Text colour to use
37 """
38 def __init__(self, text, newline, colour):
39 self.text = text
40 self.newline = newline
41 self.colour = colour
42
43 def __str__(self):
44 return 'newline=%s, colour=%s, text=%s' % (self.newline, self.colour,
45 self.text)
46
Simon Glass5f9325d2020-04-09 15:08:40 -060047def CalcAsciiLen(text):
48 """Calculate the length of a string, ignoring any ANSI sequences
49
50 Args:
51 text: Text to check
52
53 Returns:
54 Length of text, after skipping ANSI sequences
55
56 >>> col = Color(COLOR_ALWAYS)
57 >>> text = col.Color(Color.RED, 'abc')
58 >>> len(text)
59 14
60 >>> CalcAsciiLen(text)
61 3
62 >>>
63 >>> text += 'def'
64 >>> CalcAsciiLen(text)
65 6
66 >>> text += col.Color(Color.RED, 'abc')
67 >>> CalcAsciiLen(text)
68 9
69 """
70 result = ansi_escape.sub('', text)
71 return len(result)
72
73
Simon Glassfb35f9f2014-09-05 19:00:06 -060074def Print(text='', newline=True, colour=None):
75 """Handle a line of output to the terminal.
76
77 In test mode this is recorded in a list. Otherwise it is output to the
78 terminal.
79
80 Args:
81 text: Text to print
82 newline: True to add a new line at the end of the text
83 colour: Colour to use for the text
84 """
Simon Glass5f9325d2020-04-09 15:08:40 -060085 global last_print_len
86
Simon Glassfb35f9f2014-09-05 19:00:06 -060087 if print_test_mode:
88 print_test_list.append(PrintLine(text, newline, colour))
89 else:
90 if colour:
91 col = Color()
92 text = col.Color(colour, text)
Simon Glassfb35f9f2014-09-05 19:00:06 -060093 if newline:
Simon Glass82e4c642020-04-09 15:08:39 -060094 print(text)
Simon Glass5f9325d2020-04-09 15:08:40 -060095 last_print_len = None
Simon Glass9c45a4e2016-09-18 16:48:30 -060096 else:
Simon Glass82e4c642020-04-09 15:08:39 -060097 print(text, end='', flush=True)
Simon Glass5f9325d2020-04-09 15:08:40 -060098 last_print_len = CalcAsciiLen(text)
99
100def PrintClear():
101 """Clear a previously line that was printed with no newline"""
102 global last_print_len
103
104 if last_print_len:
105 print('\r%s\r' % (' '* last_print_len), end='', flush=True)
106 last_print_len = None
Simon Glassfb35f9f2014-09-05 19:00:06 -0600107
108def SetPrintTestMode():
109 """Go into test mode, where all printing is recorded"""
110 global print_test_mode
111
112 print_test_mode = True
113
114def GetPrintTestLines():
115 """Get a list of all lines output through Print()
116
117 Returns:
118 A list of PrintLine objects
119 """
120 global print_test_list
121
122 ret = print_test_list
123 print_test_list = []
124 return ret
125
126def EchoPrintTestLines():
127 """Print out the text lines collected"""
128 for line in print_test_list:
129 if line.colour:
130 col = Color()
Paul Burtonc3931342016-09-27 16:03:50 +0100131 print(col.Color(line.colour, line.text), end='')
Simon Glassfb35f9f2014-09-05 19:00:06 -0600132 else:
Paul Burtonc3931342016-09-27 16:03:50 +0100133 print(line.text, end='')
Simon Glassfb35f9f2014-09-05 19:00:06 -0600134 if line.newline:
Paul Burtonc3931342016-09-27 16:03:50 +0100135 print()
Simon Glassfb35f9f2014-09-05 19:00:06 -0600136
137
Simon Glass26132882012-01-14 15:12:45 +0000138class Color(object):
Simon Glass381fad82014-08-28 09:43:34 -0600139 """Conditionally wraps text in ANSI color escape sequences."""
140 BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8)
141 BOLD = -1
142 BRIGHT_START = '\033[1;%dm'
143 NORMAL_START = '\033[22;%dm'
144 BOLD_START = '\033[1m'
145 RESET = '\033[0m'
Simon Glass26132882012-01-14 15:12:45 +0000146
Simon Glass381fad82014-08-28 09:43:34 -0600147 def __init__(self, colored=COLOR_IF_TERMINAL):
148 """Create a new Color object, optionally disabling color output.
Simon Glass26132882012-01-14 15:12:45 +0000149
Simon Glass381fad82014-08-28 09:43:34 -0600150 Args:
151 enabled: True if color output should be enabled. If False then this
152 class will not add color codes at all.
153 """
Simon Glassb0cd3412014-08-28 09:43:35 -0600154 try:
155 self._enabled = (colored == COLOR_ALWAYS or
156 (colored == COLOR_IF_TERMINAL and
157 os.isatty(sys.stdout.fileno())))
158 except:
159 self._enabled = False
Simon Glass26132882012-01-14 15:12:45 +0000160
Simon Glass381fad82014-08-28 09:43:34 -0600161 def Start(self, color, bright=True):
162 """Returns a start color code.
Simon Glass26132882012-01-14 15:12:45 +0000163
Simon Glass381fad82014-08-28 09:43:34 -0600164 Args:
165 color: Color to use, .e.g BLACK, RED, etc.
Simon Glass26132882012-01-14 15:12:45 +0000166
Simon Glass381fad82014-08-28 09:43:34 -0600167 Returns:
168 If color is enabled, returns an ANSI sequence to start the given
169 color, otherwise returns empty string
170 """
171 if self._enabled:
172 base = self.BRIGHT_START if bright else self.NORMAL_START
173 return base % (color + 30)
174 return ''
Simon Glass26132882012-01-14 15:12:45 +0000175
Simon Glass381fad82014-08-28 09:43:34 -0600176 def Stop(self):
Anatolij Gustschinf2bcb322019-10-27 17:55:04 +0100177 """Returns a stop color code.
Simon Glass26132882012-01-14 15:12:45 +0000178
Simon Glass381fad82014-08-28 09:43:34 -0600179 Returns:
180 If color is enabled, returns an ANSI color reset sequence,
181 otherwise returns empty string
182 """
183 if self._enabled:
184 return self.RESET
185 return ''
Simon Glass26132882012-01-14 15:12:45 +0000186
Simon Glass381fad82014-08-28 09:43:34 -0600187 def Color(self, color, text, bright=True):
188 """Returns text with conditionally added color escape sequences.
Simon Glass26132882012-01-14 15:12:45 +0000189
Simon Glass381fad82014-08-28 09:43:34 -0600190 Keyword arguments:
191 color: Text color -- one of the color constants defined in this
192 class.
193 text: The text to color.
Simon Glass26132882012-01-14 15:12:45 +0000194
Simon Glass381fad82014-08-28 09:43:34 -0600195 Returns:
196 If self._enabled is False, returns the original text. If it's True,
197 returns text with color escape sequences based on the value of
198 color.
199 """
200 if not self._enabled:
201 return text
202 if color == self.BOLD:
203 start = self.BOLD_START
204 else:
205 base = self.BRIGHT_START if bright else self.NORMAL_START
206 start = base % (color + 30)
207 return start + text + self.RESET