blob: 6bd790acc3f7cdf2f05b7f4eaa9bccb2a95f4641 [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
Simon Glass14d64e32025-04-29 07:21:59 -060010from contextlib import contextmanager
11from io import StringIO
Simon Glassa9f7edb2012-12-15 10:42:01 +000012import os
Simon Glass5f9325d2020-04-09 15:08:40 -060013import re
Simon Glassbbde0532020-04-09 15:08:41 -060014import shutil
Simon Glassa9f7edb2012-12-15 10:42:01 +000015import sys
16
17# Selection of when we want our output to be colored
18COLOR_IF_TERMINAL, COLOR_ALWAYS, COLOR_NEVER = range(3)
19
Simon Glassfb35f9f2014-09-05 19:00:06 -060020# Initially, we are set up to print to the terminal
21print_test_mode = False
22print_test_list = []
23
Simon Glass5f9325d2020-04-09 15:08:40 -060024# The length of the last line printed without a newline
25last_print_len = None
26
27# credit:
28# stackoverflow.com/questions/14693701/how-can-i-remove-the-ansi-escape-sequences-from-a-string-in-python
29ansi_escape = re.compile(r'\x1b(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
30
Simon Glasse8844982025-04-29 07:22:01 -060031# True if we are capturing console output
32CAPTURING = False
33
34# Set this to False to disable output-capturing globally
35USE_CAPTURE = True
36
37
Simon Glassfb35f9f2014-09-05 19:00:06 -060038class PrintLine:
39 """A line of text output
40
41 Members:
42 text: Text line that was printed
43 newline: True to output a newline after the text
44 colour: Text colour to use
45 """
Simon Glass3db916d2020-10-29 21:46:35 -060046 def __init__(self, text, colour, newline=True, bright=True):
Simon Glassfb35f9f2014-09-05 19:00:06 -060047 self.text = text
48 self.newline = newline
49 self.colour = colour
Simon Glass3db916d2020-10-29 21:46:35 -060050 self.bright = bright
51
52 def __eq__(self, other):
53 return (self.text == other.text and
54 self.newline == other.newline and
55 self.colour == other.colour and
56 self.bright == other.bright)
Simon Glassfb35f9f2014-09-05 19:00:06 -060057
58 def __str__(self):
Simon Glass3db916d2020-10-29 21:46:35 -060059 return ("newline=%s, colour=%s, bright=%d, text='%s'" %
60 (self.newline, self.colour, self.bright, self.text))
61
Simon Glassfb35f9f2014-09-05 19:00:06 -060062
Simon Glass02811582022-01-29 14:14:18 -070063def calc_ascii_len(text):
Simon Glass5f9325d2020-04-09 15:08:40 -060064 """Calculate the length of a string, ignoring any ANSI sequences
65
Simon Glassbbde0532020-04-09 15:08:41 -060066 When displayed on a terminal, ANSI sequences don't take any space, so we
67 need to ignore them when calculating the length of a string.
68
Simon Glass5f9325d2020-04-09 15:08:40 -060069 Args:
70 text: Text to check
71
72 Returns:
73 Length of text, after skipping ANSI sequences
74
75 >>> col = Color(COLOR_ALWAYS)
Simon Glassf45d3742022-01-29 14:14:17 -070076 >>> text = col.build(Color.RED, 'abc')
Simon Glass5f9325d2020-04-09 15:08:40 -060077 >>> len(text)
78 14
Simon Glass02811582022-01-29 14:14:18 -070079 >>> calc_ascii_len(text)
Simon Glass5f9325d2020-04-09 15:08:40 -060080 3
81 >>>
82 >>> text += 'def'
Simon Glass02811582022-01-29 14:14:18 -070083 >>> calc_ascii_len(text)
Simon Glass5f9325d2020-04-09 15:08:40 -060084 6
Simon Glassf45d3742022-01-29 14:14:17 -070085 >>> text += col.build(Color.RED, 'abc')
Simon Glass02811582022-01-29 14:14:18 -070086 >>> calc_ascii_len(text)
Simon Glass5f9325d2020-04-09 15:08:40 -060087 9
88 """
89 result = ansi_escape.sub('', text)
90 return len(result)
91
Simon Glass02811582022-01-29 14:14:18 -070092def trim_ascii_len(text, size):
Simon Glassbbde0532020-04-09 15:08:41 -060093 """Trim a string containing ANSI sequences to the given ASCII length
94
95 The string is trimmed with ANSI sequences being ignored for the length
96 calculation.
97
98 >>> col = Color(COLOR_ALWAYS)
Simon Glassf45d3742022-01-29 14:14:17 -070099 >>> text = col.build(Color.RED, 'abc')
Simon Glassbbde0532020-04-09 15:08:41 -0600100 >>> len(text)
101 14
Simon Glass02811582022-01-29 14:14:18 -0700102 >>> calc_ascii_len(trim_ascii_len(text, 4))
Simon Glassbbde0532020-04-09 15:08:41 -0600103 3
Simon Glass02811582022-01-29 14:14:18 -0700104 >>> calc_ascii_len(trim_ascii_len(text, 2))
Simon Glassbbde0532020-04-09 15:08:41 -0600105 2
106 >>> text += 'def'
Simon Glass02811582022-01-29 14:14:18 -0700107 >>> calc_ascii_len(trim_ascii_len(text, 4))
Simon Glassbbde0532020-04-09 15:08:41 -0600108 4
Simon Glassf45d3742022-01-29 14:14:17 -0700109 >>> text += col.build(Color.RED, 'ghi')
Simon Glass02811582022-01-29 14:14:18 -0700110 >>> calc_ascii_len(trim_ascii_len(text, 7))
Simon Glassbbde0532020-04-09 15:08:41 -0600111 7
112 """
Simon Glass02811582022-01-29 14:14:18 -0700113 if calc_ascii_len(text) < size:
Simon Glassbbde0532020-04-09 15:08:41 -0600114 return text
115 pos = 0
116 out = ''
117 left = size
118
119 # Work through each ANSI sequence in turn
120 for m in ansi_escape.finditer(text):
121 # Find the text before the sequence and add it to our string, making
122 # sure it doesn't overflow
123 before = text[pos:m.start()]
124 toadd = before[:left]
125 out += toadd
126
127 # Figure out how much non-ANSI space we have left
128 left -= len(toadd)
129
130 # Add the ANSI sequence and move to the position immediately after it
131 out += m.group()
132 pos = m.start() + len(m.group())
133
134 # Deal with text after the last ANSI sequence
135 after = text[pos:]
136 toadd = after[:left]
137 out += toadd
138
139 return out
140
Simon Glass5f9325d2020-04-09 15:08:40 -0600141
Simon Glass2027ddd2025-04-29 07:22:02 -0600142def tprint(text='', newline=True, colour=None, limit_to_line=False,
143 bright=True, back=None, col=None):
Simon Glassfb35f9f2014-09-05 19:00:06 -0600144 """Handle a line of output to the terminal.
145
146 In test mode this is recorded in a list. Otherwise it is output to the
147 terminal.
148
149 Args:
150 text: Text to print
151 newline: True to add a new line at the end of the text
152 colour: Colour to use for the text
153 """
Simon Glass5f9325d2020-04-09 15:08:40 -0600154 global last_print_len
155
Simon Glassfb35f9f2014-09-05 19:00:06 -0600156 if print_test_mode:
Simon Glass3db916d2020-10-29 21:46:35 -0600157 print_test_list.append(PrintLine(text, colour, newline, bright))
Simon Glassfb35f9f2014-09-05 19:00:06 -0600158 else:
Simon Glass2027ddd2025-04-29 07:22:02 -0600159 if colour is not None:
160 if not col:
161 col = Color()
162 text = col.build(colour, text, bright=bright, back=back)
Simon Glassfb35f9f2014-09-05 19:00:06 -0600163 if newline:
Simon Glass82e4c642020-04-09 15:08:39 -0600164 print(text)
Simon Glass5f9325d2020-04-09 15:08:40 -0600165 last_print_len = None
Simon Glass9c45a4e2016-09-18 16:48:30 -0600166 else:
Simon Glassbbde0532020-04-09 15:08:41 -0600167 if limit_to_line:
168 cols = shutil.get_terminal_size().columns
Simon Glass02811582022-01-29 14:14:18 -0700169 text = trim_ascii_len(text, cols)
Simon Glass82e4c642020-04-09 15:08:39 -0600170 print(text, end='', flush=True)
Simon Glass02811582022-01-29 14:14:18 -0700171 last_print_len = calc_ascii_len(text)
Simon Glass5f9325d2020-04-09 15:08:40 -0600172
Simon Glass02811582022-01-29 14:14:18 -0700173def print_clear():
Simon Glass5f9325d2020-04-09 15:08:40 -0600174 """Clear a previously line that was printed with no newline"""
175 global last_print_len
176
177 if last_print_len:
Simon Glassc229d322024-06-23 11:55:15 -0600178 if print_test_mode:
179 print_test_list.append(PrintLine(None, None, None, None))
180 else:
181 print('\r%s\r' % (' '* last_print_len), end='', flush=True)
182 last_print_len = None
Simon Glassfb35f9f2014-09-05 19:00:06 -0600183
Simon Glass02811582022-01-29 14:14:18 -0700184def set_print_test_mode(enable=True):
Simon Glassfb35f9f2014-09-05 19:00:06 -0600185 """Go into test mode, where all printing is recorded"""
186 global print_test_mode
187
Simon Glass3db916d2020-10-29 21:46:35 -0600188 print_test_mode = enable
Simon Glass02811582022-01-29 14:14:18 -0700189 get_print_test_lines()
Simon Glassfb35f9f2014-09-05 19:00:06 -0600190
Simon Glass02811582022-01-29 14:14:18 -0700191def get_print_test_lines():
192 """Get a list of all lines output through tprint()
Simon Glassfb35f9f2014-09-05 19:00:06 -0600193
194 Returns:
195 A list of PrintLine objects
196 """
197 global print_test_list
198
199 ret = print_test_list
200 print_test_list = []
201 return ret
202
Simon Glass02811582022-01-29 14:14:18 -0700203def echo_print_test_lines():
Simon Glassfb35f9f2014-09-05 19:00:06 -0600204 """Print out the text lines collected"""
205 for line in print_test_list:
206 if line.colour:
207 col = Color()
Simon Glassf45d3742022-01-29 14:14:17 -0700208 print(col.build(line.colour, line.text), end='')
Simon Glassfb35f9f2014-09-05 19:00:06 -0600209 else:
Paul Burtonc3931342016-09-27 16:03:50 +0100210 print(line.text, end='')
Simon Glassfb35f9f2014-09-05 19:00:06 -0600211 if line.newline:
Paul Burtonc3931342016-09-27 16:03:50 +0100212 print()
Simon Glassfb35f9f2014-09-05 19:00:06 -0600213
214
Simon Glass26132882012-01-14 15:12:45 +0000215class Color(object):
Simon Glass381fad82014-08-28 09:43:34 -0600216 """Conditionally wraps text in ANSI color escape sequences."""
217 BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8)
218 BOLD = -1
Simon Glass2027ddd2025-04-29 07:22:02 -0600219 BRIGHT_START = '\033[1;%d%sm'
220 NORMAL_START = '\033[22;%d%sm'
Simon Glass381fad82014-08-28 09:43:34 -0600221 BOLD_START = '\033[1m'
Simon Glass2027ddd2025-04-29 07:22:02 -0600222 BACK_EXTRA = ';%d'
Simon Glass381fad82014-08-28 09:43:34 -0600223 RESET = '\033[0m'
Simon Glass26132882012-01-14 15:12:45 +0000224
Simon Glass381fad82014-08-28 09:43:34 -0600225 def __init__(self, colored=COLOR_IF_TERMINAL):
226 """Create a new Color object, optionally disabling color output.
Simon Glass26132882012-01-14 15:12:45 +0000227
Simon Glass381fad82014-08-28 09:43:34 -0600228 Args:
229 enabled: True if color output should be enabled. If False then this
230 class will not add color codes at all.
231 """
Simon Glassb0cd3412014-08-28 09:43:35 -0600232 try:
233 self._enabled = (colored == COLOR_ALWAYS or
234 (colored == COLOR_IF_TERMINAL and
235 os.isatty(sys.stdout.fileno())))
236 except:
237 self._enabled = False
Simon Glass26132882012-01-14 15:12:45 +0000238
Simon Glass2027ddd2025-04-29 07:22:02 -0600239 def enabled(self):
240 """Check if colour is enabled
241
242 Return: True if enabled, else False
243 """
244 return self._enabled
245
246 def start(self, color, bright=True, back=None):
Simon Glass381fad82014-08-28 09:43:34 -0600247 """Returns a start color code.
Simon Glass26132882012-01-14 15:12:45 +0000248
Simon Glass381fad82014-08-28 09:43:34 -0600249 Args:
250 color: Color to use, .e.g BLACK, RED, etc.
Simon Glass26132882012-01-14 15:12:45 +0000251
Simon Glass381fad82014-08-28 09:43:34 -0600252 Returns:
253 If color is enabled, returns an ANSI sequence to start the given
254 color, otherwise returns empty string
255 """
256 if self._enabled:
Simon Glass2027ddd2025-04-29 07:22:02 -0600257 if color == self.BOLD:
258 return self.BOLD_START
Simon Glass381fad82014-08-28 09:43:34 -0600259 base = self.BRIGHT_START if bright else self.NORMAL_START
Simon Glass2027ddd2025-04-29 07:22:02 -0600260 extra = self.BACK_EXTRA % (back + 40) if back else ''
261 return base % (color + 30, extra)
Simon Glass381fad82014-08-28 09:43:34 -0600262 return ''
Simon Glass26132882012-01-14 15:12:45 +0000263
Simon Glass02811582022-01-29 14:14:18 -0700264 def stop(self):
Anatolij Gustschinf2bcb322019-10-27 17:55:04 +0100265 """Returns a stop color code.
Simon Glass26132882012-01-14 15:12:45 +0000266
Simon Glass381fad82014-08-28 09:43:34 -0600267 Returns:
268 If color is enabled, returns an ANSI color reset sequence,
269 otherwise returns empty string
270 """
271 if self._enabled:
272 return self.RESET
273 return ''
Simon Glass26132882012-01-14 15:12:45 +0000274
Simon Glass2027ddd2025-04-29 07:22:02 -0600275 def build(self, color, text, bright=True, back=None):
Simon Glass381fad82014-08-28 09:43:34 -0600276 """Returns text with conditionally added color escape sequences.
Simon Glass26132882012-01-14 15:12:45 +0000277
Simon Glass381fad82014-08-28 09:43:34 -0600278 Keyword arguments:
279 color: Text color -- one of the color constants defined in this
280 class.
281 text: The text to color.
Simon Glass26132882012-01-14 15:12:45 +0000282
Simon Glass381fad82014-08-28 09:43:34 -0600283 Returns:
284 If self._enabled is False, returns the original text. If it's True,
285 returns text with color escape sequences based on the value of
286 color.
287 """
288 if not self._enabled:
289 return text
Simon Glass2027ddd2025-04-29 07:22:02 -0600290 return self.start(color, bright, back) + text + self.RESET
Simon Glass14d64e32025-04-29 07:21:59 -0600291
292
293# Use this to suppress stdout/stderr output:
294# with terminal.capture() as (stdout, stderr)
295# ...do something...
296@contextmanager
297def capture():
Simon Glasse8844982025-04-29 07:22:01 -0600298 global CAPTURING
299
Simon Glass14d64e32025-04-29 07:21:59 -0600300 capture_out, capture_err = StringIO(), StringIO()
301 old_out, old_err = sys.stdout, sys.stderr
302 try:
Simon Glasse8844982025-04-29 07:22:01 -0600303 CAPTURING = True
Simon Glass14d64e32025-04-29 07:21:59 -0600304 sys.stdout, sys.stderr = capture_out, capture_err
305 yield capture_out, capture_err
306 finally:
307 sys.stdout, sys.stderr = old_out, old_err
Simon Glasse8844982025-04-29 07:22:01 -0600308 CAPTURING = False
309 if not USE_CAPTURE:
310 sys.stdout.write(capture_out.getvalue())
311 sys.stderr.write(capture_err.getvalue())