blob: 60dbce3ce1ffd7da78a4141220b4ce47defd4078 [file] [log] [blame]
Tom Rini83d290c2018-05-06 17:58:06 -04001# SPDX-License-Identifier: GPL-2.0+
Simon Glass0d24de92012-01-14 15:12:45 +00002# Copyright (c) 2011 The Chromium OS Authors.
3#
Simon Glass0d24de92012-01-14 15:12:45 +00004
5"""Terminal utilities
6
7This module handles terminal interaction including ANSI color codes.
8"""
9
Simon Glassbbd01432012-12-15 10:42:01 +000010import os
Simon Glass37b224f2020-04-09 15:08:40 -060011import re
Simon Glass1e130472020-04-09 15:08:41 -060012import shutil
Simon Glassbbd01432012-12-15 10:42:01 +000013import sys
14
15# Selection of when we want our output to be colored
16COLOR_IF_TERMINAL, COLOR_ALWAYS, COLOR_NEVER = range(3)
17
Simon Glass3c6c0f82014-09-05 19:00:06 -060018# Initially, we are set up to print to the terminal
19print_test_mode = False
20print_test_list = []
21
Simon Glass37b224f2020-04-09 15:08:40 -060022# The length of the last line printed without a newline
23last_print_len = None
24
25# credit:
26# stackoverflow.com/questions/14693701/how-can-i-remove-the-ansi-escape-sequences-from-a-string-in-python
27ansi_escape = re.compile(r'\x1b(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
28
Simon Glass3c6c0f82014-09-05 19:00:06 -060029class PrintLine:
30 """A line of text output
31
32 Members:
33 text: Text line that was printed
34 newline: True to output a newline after the text
35 colour: Text colour to use
36 """
37 def __init__(self, text, newline, colour):
38 self.text = text
39 self.newline = newline
40 self.colour = colour
41
42 def __str__(self):
43 return 'newline=%s, colour=%s, text=%s' % (self.newline, self.colour,
44 self.text)
45
Simon Glass37b224f2020-04-09 15:08:40 -060046def CalcAsciiLen(text):
47 """Calculate the length of a string, ignoring any ANSI sequences
48
Simon Glass1e130472020-04-09 15:08:41 -060049 When displayed on a terminal, ANSI sequences don't take any space, so we
50 need to ignore them when calculating the length of a string.
51
Simon Glass37b224f2020-04-09 15:08:40 -060052 Args:
53 text: Text to check
54
55 Returns:
56 Length of text, after skipping ANSI sequences
57
58 >>> col = Color(COLOR_ALWAYS)
59 >>> text = col.Color(Color.RED, 'abc')
60 >>> len(text)
61 14
62 >>> CalcAsciiLen(text)
63 3
64 >>>
65 >>> text += 'def'
66 >>> CalcAsciiLen(text)
67 6
68 >>> text += col.Color(Color.RED, 'abc')
69 >>> CalcAsciiLen(text)
70 9
71 """
72 result = ansi_escape.sub('', text)
73 return len(result)
74
Simon Glass1e130472020-04-09 15:08:41 -060075def TrimAsciiLen(text, size):
76 """Trim a string containing ANSI sequences to the given ASCII length
Simon Glass37b224f2020-04-09 15:08:40 -060077
Simon Glass1e130472020-04-09 15:08:41 -060078 The string is trimmed with ANSI sequences being ignored for the length
79 calculation.
80
81 >>> col = Color(COLOR_ALWAYS)
82 >>> text = col.Color(Color.RED, 'abc')
83 >>> len(text)
84 14
85 >>> CalcAsciiLen(TrimAsciiLen(text, 4))
86 3
87 >>> CalcAsciiLen(TrimAsciiLen(text, 2))
88 2
89 >>> text += 'def'
90 >>> CalcAsciiLen(TrimAsciiLen(text, 4))
91 4
92 >>> text += col.Color(Color.RED, 'ghi')
93 >>> CalcAsciiLen(TrimAsciiLen(text, 7))
94 7
95 """
96 if CalcAsciiLen(text) < size:
97 return text
98 pos = 0
99 out = ''
100 left = size
101
102 # Work through each ANSI sequence in turn
103 for m in ansi_escape.finditer(text):
104 # Find the text before the sequence and add it to our string, making
105 # sure it doesn't overflow
106 before = text[pos:m.start()]
107 toadd = before[:left]
108 out += toadd
109
110 # Figure out how much non-ANSI space we have left
111 left -= len(toadd)
112
113 # Add the ANSI sequence and move to the position immediately after it
114 out += m.group()
115 pos = m.start() + len(m.group())
116
117 # Deal with text after the last ANSI sequence
118 after = text[pos:]
119 toadd = after[:left]
120 out += toadd
121
122 return out
123
124
Simon Glass3c541c02020-07-05 21:41:56 -0600125def Print(text='', newline=True, colour=None, limit_to_line=False, bright=True):
Simon Glass3c6c0f82014-09-05 19:00:06 -0600126 """Handle a line of output to the terminal.
127
128 In test mode this is recorded in a list. Otherwise it is output to the
129 terminal.
130
131 Args:
132 text: Text to print
133 newline: True to add a new line at the end of the text
134 colour: Colour to use for the text
135 """
Simon Glass37b224f2020-04-09 15:08:40 -0600136 global last_print_len
137
Simon Glass3c6c0f82014-09-05 19:00:06 -0600138 if print_test_mode:
139 print_test_list.append(PrintLine(text, newline, colour))
140 else:
141 if colour:
142 col = Color()
Simon Glass3c541c02020-07-05 21:41:56 -0600143 text = col.Color(colour, text, bright=bright)
Simon Glass3c6c0f82014-09-05 19:00:06 -0600144 if newline:
Simon Glassa84eb162020-04-09 15:08:39 -0600145 print(text)
Simon Glass37b224f2020-04-09 15:08:40 -0600146 last_print_len = None
Simon Glass8b4919e2016-09-18 16:48:30 -0600147 else:
Simon Glass1e130472020-04-09 15:08:41 -0600148 if limit_to_line:
149 cols = shutil.get_terminal_size().columns
150 text = TrimAsciiLen(text, cols)
Simon Glassa84eb162020-04-09 15:08:39 -0600151 print(text, end='', flush=True)
Simon Glass37b224f2020-04-09 15:08:40 -0600152 last_print_len = CalcAsciiLen(text)
153
154def PrintClear():
155 """Clear a previously line that was printed with no newline"""
156 global last_print_len
157
158 if last_print_len:
159 print('\r%s\r' % (' '* last_print_len), end='', flush=True)
160 last_print_len = None
Simon Glass3c6c0f82014-09-05 19:00:06 -0600161
162def SetPrintTestMode():
163 """Go into test mode, where all printing is recorded"""
164 global print_test_mode
165
166 print_test_mode = True
167
168def GetPrintTestLines():
169 """Get a list of all lines output through Print()
170
171 Returns:
172 A list of PrintLine objects
173 """
174 global print_test_list
175
176 ret = print_test_list
177 print_test_list = []
178 return ret
179
180def EchoPrintTestLines():
181 """Print out the text lines collected"""
182 for line in print_test_list:
183 if line.colour:
184 col = Color()
Paul Burtona920a172016-09-27 16:03:50 +0100185 print(col.Color(line.colour, line.text), end='')
Simon Glass3c6c0f82014-09-05 19:00:06 -0600186 else:
Paul Burtona920a172016-09-27 16:03:50 +0100187 print(line.text, end='')
Simon Glass3c6c0f82014-09-05 19:00:06 -0600188 if line.newline:
Paul Burtona920a172016-09-27 16:03:50 +0100189 print()
Simon Glass3c6c0f82014-09-05 19:00:06 -0600190
191
Simon Glass0d24de92012-01-14 15:12:45 +0000192class Color(object):
Simon Glass6ba57372014-08-28 09:43:34 -0600193 """Conditionally wraps text in ANSI color escape sequences."""
194 BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8)
195 BOLD = -1
196 BRIGHT_START = '\033[1;%dm'
197 NORMAL_START = '\033[22;%dm'
198 BOLD_START = '\033[1m'
199 RESET = '\033[0m'
Simon Glass0d24de92012-01-14 15:12:45 +0000200
Simon Glass6ba57372014-08-28 09:43:34 -0600201 def __init__(self, colored=COLOR_IF_TERMINAL):
202 """Create a new Color object, optionally disabling color output.
Simon Glass0d24de92012-01-14 15:12:45 +0000203
Simon Glass6ba57372014-08-28 09:43:34 -0600204 Args:
205 enabled: True if color output should be enabled. If False then this
206 class will not add color codes at all.
207 """
Simon Glasse752edc2014-08-28 09:43:35 -0600208 try:
209 self._enabled = (colored == COLOR_ALWAYS or
210 (colored == COLOR_IF_TERMINAL and
211 os.isatty(sys.stdout.fileno())))
212 except:
213 self._enabled = False
Simon Glass0d24de92012-01-14 15:12:45 +0000214
Simon Glass6ba57372014-08-28 09:43:34 -0600215 def Start(self, color, bright=True):
216 """Returns a start color code.
Simon Glass0d24de92012-01-14 15:12:45 +0000217
Simon Glass6ba57372014-08-28 09:43:34 -0600218 Args:
219 color: Color to use, .e.g BLACK, RED, etc.
Simon Glass0d24de92012-01-14 15:12:45 +0000220
Simon Glass6ba57372014-08-28 09:43:34 -0600221 Returns:
222 If color is enabled, returns an ANSI sequence to start the given
223 color, otherwise returns empty string
224 """
225 if self._enabled:
226 base = self.BRIGHT_START if bright else self.NORMAL_START
227 return base % (color + 30)
228 return ''
Simon Glass0d24de92012-01-14 15:12:45 +0000229
Simon Glass6ba57372014-08-28 09:43:34 -0600230 def Stop(self):
Anatolij Gustschinab4a6ab2019-10-27 17:55:04 +0100231 """Returns a stop color code.
Simon Glass0d24de92012-01-14 15:12:45 +0000232
Simon Glass6ba57372014-08-28 09:43:34 -0600233 Returns:
234 If color is enabled, returns an ANSI color reset sequence,
235 otherwise returns empty string
236 """
237 if self._enabled:
238 return self.RESET
239 return ''
Simon Glass0d24de92012-01-14 15:12:45 +0000240
Simon Glass6ba57372014-08-28 09:43:34 -0600241 def Color(self, color, text, bright=True):
242 """Returns text with conditionally added color escape sequences.
Simon Glass0d24de92012-01-14 15:12:45 +0000243
Simon Glass6ba57372014-08-28 09:43:34 -0600244 Keyword arguments:
245 color: Text color -- one of the color constants defined in this
246 class.
247 text: The text to color.
Simon Glass0d24de92012-01-14 15:12:45 +0000248
Simon Glass6ba57372014-08-28 09:43:34 -0600249 Returns:
250 If self._enabled is False, returns the original text. If it's True,
251 returns text with color escape sequences based on the value of
252 color.
253 """
254 if not self._enabled:
255 return text
256 if color == self.BOLD:
257 start = self.BOLD_START
258 else:
259 base = self.BRIGHT_START if bright else self.NORMAL_START
260 start = base % (color + 30)
261 return start + text + self.RESET