Tom Rini | 10e4779 | 2018-05-06 17:58:06 -0400 | [diff] [blame] | 1 | # SPDX-License-Identifier: GPL-2.0+ |
Simon Glass | 2613288 | 2012-01-14 15:12:45 +0000 | [diff] [blame] | 2 | # Copyright (c) 2011 The Chromium OS Authors. |
| 3 | # |
Simon Glass | 2613288 | 2012-01-14 15:12:45 +0000 | [diff] [blame] | 4 | |
| 5 | """Terminal utilities |
| 6 | |
| 7 | This module handles terminal interaction including ANSI color codes. |
| 8 | """ |
| 9 | |
Simon Glass | 14d64e3 | 2025-04-29 07:21:59 -0600 | [diff] [blame] | 10 | from contextlib import contextmanager |
| 11 | from io import StringIO |
Simon Glass | a9f7edb | 2012-12-15 10:42:01 +0000 | [diff] [blame] | 12 | import os |
Simon Glass | 5f9325d | 2020-04-09 15:08:40 -0600 | [diff] [blame] | 13 | import re |
Simon Glass | bbde053 | 2020-04-09 15:08:41 -0600 | [diff] [blame] | 14 | import shutil |
Simon Glass | e46c9cf | 2025-04-29 07:22:03 -0600 | [diff] [blame^] | 15 | import subprocess |
Simon Glass | a9f7edb | 2012-12-15 10:42:01 +0000 | [diff] [blame] | 16 | import sys |
| 17 | |
| 18 | # Selection of when we want our output to be colored |
| 19 | COLOR_IF_TERMINAL, COLOR_ALWAYS, COLOR_NEVER = range(3) |
| 20 | |
Simon Glass | fb35f9f | 2014-09-05 19:00:06 -0600 | [diff] [blame] | 21 | # Initially, we are set up to print to the terminal |
| 22 | print_test_mode = False |
| 23 | print_test_list = [] |
| 24 | |
Simon Glass | 5f9325d | 2020-04-09 15:08:40 -0600 | [diff] [blame] | 25 | # The length of the last line printed without a newline |
| 26 | last_print_len = None |
| 27 | |
| 28 | # credit: |
| 29 | # stackoverflow.com/questions/14693701/how-can-i-remove-the-ansi-escape-sequences-from-a-string-in-python |
| 30 | ansi_escape = re.compile(r'\x1b(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])') |
| 31 | |
Simon Glass | e884498 | 2025-04-29 07:22:01 -0600 | [diff] [blame] | 32 | # True if we are capturing console output |
| 33 | CAPTURING = False |
| 34 | |
| 35 | # Set this to False to disable output-capturing globally |
| 36 | USE_CAPTURE = True |
| 37 | |
| 38 | |
Simon Glass | fb35f9f | 2014-09-05 19:00:06 -0600 | [diff] [blame] | 39 | class PrintLine: |
| 40 | """A line of text output |
| 41 | |
| 42 | Members: |
| 43 | text: Text line that was printed |
| 44 | newline: True to output a newline after the text |
| 45 | colour: Text colour to use |
| 46 | """ |
Simon Glass | 3db916d | 2020-10-29 21:46:35 -0600 | [diff] [blame] | 47 | def __init__(self, text, colour, newline=True, bright=True): |
Simon Glass | fb35f9f | 2014-09-05 19:00:06 -0600 | [diff] [blame] | 48 | self.text = text |
| 49 | self.newline = newline |
| 50 | self.colour = colour |
Simon Glass | 3db916d | 2020-10-29 21:46:35 -0600 | [diff] [blame] | 51 | self.bright = bright |
| 52 | |
| 53 | def __eq__(self, other): |
| 54 | return (self.text == other.text and |
| 55 | self.newline == other.newline and |
| 56 | self.colour == other.colour and |
| 57 | self.bright == other.bright) |
Simon Glass | fb35f9f | 2014-09-05 19:00:06 -0600 | [diff] [blame] | 58 | |
| 59 | def __str__(self): |
Simon Glass | 3db916d | 2020-10-29 21:46:35 -0600 | [diff] [blame] | 60 | return ("newline=%s, colour=%s, bright=%d, text='%s'" % |
| 61 | (self.newline, self.colour, self.bright, self.text)) |
| 62 | |
Simon Glass | fb35f9f | 2014-09-05 19:00:06 -0600 | [diff] [blame] | 63 | |
Simon Glass | 0281158 | 2022-01-29 14:14:18 -0700 | [diff] [blame] | 64 | def calc_ascii_len(text): |
Simon Glass | 5f9325d | 2020-04-09 15:08:40 -0600 | [diff] [blame] | 65 | """Calculate the length of a string, ignoring any ANSI sequences |
| 66 | |
Simon Glass | bbde053 | 2020-04-09 15:08:41 -0600 | [diff] [blame] | 67 | When displayed on a terminal, ANSI sequences don't take any space, so we |
| 68 | need to ignore them when calculating the length of a string. |
| 69 | |
Simon Glass | 5f9325d | 2020-04-09 15:08:40 -0600 | [diff] [blame] | 70 | Args: |
| 71 | text: Text to check |
| 72 | |
| 73 | Returns: |
| 74 | Length of text, after skipping ANSI sequences |
| 75 | |
| 76 | >>> col = Color(COLOR_ALWAYS) |
Simon Glass | f45d374 | 2022-01-29 14:14:17 -0700 | [diff] [blame] | 77 | >>> text = col.build(Color.RED, 'abc') |
Simon Glass | 5f9325d | 2020-04-09 15:08:40 -0600 | [diff] [blame] | 78 | >>> len(text) |
| 79 | 14 |
Simon Glass | 0281158 | 2022-01-29 14:14:18 -0700 | [diff] [blame] | 80 | >>> calc_ascii_len(text) |
Simon Glass | 5f9325d | 2020-04-09 15:08:40 -0600 | [diff] [blame] | 81 | 3 |
| 82 | >>> |
| 83 | >>> text += 'def' |
Simon Glass | 0281158 | 2022-01-29 14:14:18 -0700 | [diff] [blame] | 84 | >>> calc_ascii_len(text) |
Simon Glass | 5f9325d | 2020-04-09 15:08:40 -0600 | [diff] [blame] | 85 | 6 |
Simon Glass | f45d374 | 2022-01-29 14:14:17 -0700 | [diff] [blame] | 86 | >>> text += col.build(Color.RED, 'abc') |
Simon Glass | 0281158 | 2022-01-29 14:14:18 -0700 | [diff] [blame] | 87 | >>> calc_ascii_len(text) |
Simon Glass | 5f9325d | 2020-04-09 15:08:40 -0600 | [diff] [blame] | 88 | 9 |
| 89 | """ |
| 90 | result = ansi_escape.sub('', text) |
| 91 | return len(result) |
| 92 | |
Simon Glass | 0281158 | 2022-01-29 14:14:18 -0700 | [diff] [blame] | 93 | def trim_ascii_len(text, size): |
Simon Glass | bbde053 | 2020-04-09 15:08:41 -0600 | [diff] [blame] | 94 | """Trim a string containing ANSI sequences to the given ASCII length |
| 95 | |
| 96 | The string is trimmed with ANSI sequences being ignored for the length |
| 97 | calculation. |
| 98 | |
| 99 | >>> col = Color(COLOR_ALWAYS) |
Simon Glass | f45d374 | 2022-01-29 14:14:17 -0700 | [diff] [blame] | 100 | >>> text = col.build(Color.RED, 'abc') |
Simon Glass | bbde053 | 2020-04-09 15:08:41 -0600 | [diff] [blame] | 101 | >>> len(text) |
| 102 | 14 |
Simon Glass | 0281158 | 2022-01-29 14:14:18 -0700 | [diff] [blame] | 103 | >>> calc_ascii_len(trim_ascii_len(text, 4)) |
Simon Glass | bbde053 | 2020-04-09 15:08:41 -0600 | [diff] [blame] | 104 | 3 |
Simon Glass | 0281158 | 2022-01-29 14:14:18 -0700 | [diff] [blame] | 105 | >>> calc_ascii_len(trim_ascii_len(text, 2)) |
Simon Glass | bbde053 | 2020-04-09 15:08:41 -0600 | [diff] [blame] | 106 | 2 |
| 107 | >>> text += 'def' |
Simon Glass | 0281158 | 2022-01-29 14:14:18 -0700 | [diff] [blame] | 108 | >>> calc_ascii_len(trim_ascii_len(text, 4)) |
Simon Glass | bbde053 | 2020-04-09 15:08:41 -0600 | [diff] [blame] | 109 | 4 |
Simon Glass | f45d374 | 2022-01-29 14:14:17 -0700 | [diff] [blame] | 110 | >>> text += col.build(Color.RED, 'ghi') |
Simon Glass | 0281158 | 2022-01-29 14:14:18 -0700 | [diff] [blame] | 111 | >>> calc_ascii_len(trim_ascii_len(text, 7)) |
Simon Glass | bbde053 | 2020-04-09 15:08:41 -0600 | [diff] [blame] | 112 | 7 |
| 113 | """ |
Simon Glass | 0281158 | 2022-01-29 14:14:18 -0700 | [diff] [blame] | 114 | if calc_ascii_len(text) < size: |
Simon Glass | bbde053 | 2020-04-09 15:08:41 -0600 | [diff] [blame] | 115 | return text |
| 116 | pos = 0 |
| 117 | out = '' |
| 118 | left = size |
| 119 | |
| 120 | # Work through each ANSI sequence in turn |
| 121 | for m in ansi_escape.finditer(text): |
| 122 | # Find the text before the sequence and add it to our string, making |
| 123 | # sure it doesn't overflow |
| 124 | before = text[pos:m.start()] |
| 125 | toadd = before[:left] |
| 126 | out += toadd |
| 127 | |
| 128 | # Figure out how much non-ANSI space we have left |
| 129 | left -= len(toadd) |
| 130 | |
| 131 | # Add the ANSI sequence and move to the position immediately after it |
| 132 | out += m.group() |
| 133 | pos = m.start() + len(m.group()) |
| 134 | |
| 135 | # Deal with text after the last ANSI sequence |
| 136 | after = text[pos:] |
| 137 | toadd = after[:left] |
| 138 | out += toadd |
| 139 | |
| 140 | return out |
| 141 | |
Simon Glass | 5f9325d | 2020-04-09 15:08:40 -0600 | [diff] [blame] | 142 | |
Simon Glass | 2027ddd | 2025-04-29 07:22:02 -0600 | [diff] [blame] | 143 | def tprint(text='', newline=True, colour=None, limit_to_line=False, |
| 144 | bright=True, back=None, col=None): |
Simon Glass | fb35f9f | 2014-09-05 19:00:06 -0600 | [diff] [blame] | 145 | """Handle a line of output to the terminal. |
| 146 | |
| 147 | In test mode this is recorded in a list. Otherwise it is output to the |
| 148 | terminal. |
| 149 | |
| 150 | Args: |
| 151 | text: Text to print |
| 152 | newline: True to add a new line at the end of the text |
| 153 | colour: Colour to use for the text |
| 154 | """ |
Simon Glass | 5f9325d | 2020-04-09 15:08:40 -0600 | [diff] [blame] | 155 | global last_print_len |
| 156 | |
Simon Glass | fb35f9f | 2014-09-05 19:00:06 -0600 | [diff] [blame] | 157 | if print_test_mode: |
Simon Glass | 3db916d | 2020-10-29 21:46:35 -0600 | [diff] [blame] | 158 | print_test_list.append(PrintLine(text, colour, newline, bright)) |
Simon Glass | fb35f9f | 2014-09-05 19:00:06 -0600 | [diff] [blame] | 159 | else: |
Simon Glass | 2027ddd | 2025-04-29 07:22:02 -0600 | [diff] [blame] | 160 | if colour is not None: |
| 161 | if not col: |
| 162 | col = Color() |
| 163 | text = col.build(colour, text, bright=bright, back=back) |
Simon Glass | fb35f9f | 2014-09-05 19:00:06 -0600 | [diff] [blame] | 164 | if newline: |
Simon Glass | 82e4c64 | 2020-04-09 15:08:39 -0600 | [diff] [blame] | 165 | print(text) |
Simon Glass | 5f9325d | 2020-04-09 15:08:40 -0600 | [diff] [blame] | 166 | last_print_len = None |
Simon Glass | 9c45a4e | 2016-09-18 16:48:30 -0600 | [diff] [blame] | 167 | else: |
Simon Glass | bbde053 | 2020-04-09 15:08:41 -0600 | [diff] [blame] | 168 | if limit_to_line: |
| 169 | cols = shutil.get_terminal_size().columns |
Simon Glass | 0281158 | 2022-01-29 14:14:18 -0700 | [diff] [blame] | 170 | text = trim_ascii_len(text, cols) |
Simon Glass | 82e4c64 | 2020-04-09 15:08:39 -0600 | [diff] [blame] | 171 | print(text, end='', flush=True) |
Simon Glass | 0281158 | 2022-01-29 14:14:18 -0700 | [diff] [blame] | 172 | last_print_len = calc_ascii_len(text) |
Simon Glass | 5f9325d | 2020-04-09 15:08:40 -0600 | [diff] [blame] | 173 | |
Simon Glass | 0281158 | 2022-01-29 14:14:18 -0700 | [diff] [blame] | 174 | def print_clear(): |
Simon Glass | 5f9325d | 2020-04-09 15:08:40 -0600 | [diff] [blame] | 175 | """Clear a previously line that was printed with no newline""" |
| 176 | global last_print_len |
| 177 | |
| 178 | if last_print_len: |
Simon Glass | c229d32 | 2024-06-23 11:55:15 -0600 | [diff] [blame] | 179 | if print_test_mode: |
| 180 | print_test_list.append(PrintLine(None, None, None, None)) |
| 181 | else: |
| 182 | print('\r%s\r' % (' '* last_print_len), end='', flush=True) |
| 183 | last_print_len = None |
Simon Glass | fb35f9f | 2014-09-05 19:00:06 -0600 | [diff] [blame] | 184 | |
Simon Glass | 0281158 | 2022-01-29 14:14:18 -0700 | [diff] [blame] | 185 | def set_print_test_mode(enable=True): |
Simon Glass | fb35f9f | 2014-09-05 19:00:06 -0600 | [diff] [blame] | 186 | """Go into test mode, where all printing is recorded""" |
| 187 | global print_test_mode |
| 188 | |
Simon Glass | 3db916d | 2020-10-29 21:46:35 -0600 | [diff] [blame] | 189 | print_test_mode = enable |
Simon Glass | 0281158 | 2022-01-29 14:14:18 -0700 | [diff] [blame] | 190 | get_print_test_lines() |
Simon Glass | fb35f9f | 2014-09-05 19:00:06 -0600 | [diff] [blame] | 191 | |
Simon Glass | 0281158 | 2022-01-29 14:14:18 -0700 | [diff] [blame] | 192 | def get_print_test_lines(): |
| 193 | """Get a list of all lines output through tprint() |
Simon Glass | fb35f9f | 2014-09-05 19:00:06 -0600 | [diff] [blame] | 194 | |
| 195 | Returns: |
| 196 | A list of PrintLine objects |
| 197 | """ |
| 198 | global print_test_list |
| 199 | |
| 200 | ret = print_test_list |
| 201 | print_test_list = [] |
| 202 | return ret |
| 203 | |
Simon Glass | 0281158 | 2022-01-29 14:14:18 -0700 | [diff] [blame] | 204 | def echo_print_test_lines(): |
Simon Glass | fb35f9f | 2014-09-05 19:00:06 -0600 | [diff] [blame] | 205 | """Print out the text lines collected""" |
| 206 | for line in print_test_list: |
| 207 | if line.colour: |
| 208 | col = Color() |
Simon Glass | f45d374 | 2022-01-29 14:14:17 -0700 | [diff] [blame] | 209 | print(col.build(line.colour, line.text), end='') |
Simon Glass | fb35f9f | 2014-09-05 19:00:06 -0600 | [diff] [blame] | 210 | else: |
Paul Burton | c393134 | 2016-09-27 16:03:50 +0100 | [diff] [blame] | 211 | print(line.text, end='') |
Simon Glass | fb35f9f | 2014-09-05 19:00:06 -0600 | [diff] [blame] | 212 | if line.newline: |
Paul Burton | c393134 | 2016-09-27 16:03:50 +0100 | [diff] [blame] | 213 | print() |
Simon Glass | fb35f9f | 2014-09-05 19:00:06 -0600 | [diff] [blame] | 214 | |
Simon Glass | e46c9cf | 2025-04-29 07:22:03 -0600 | [diff] [blame^] | 215 | def have_terminal(): |
| 216 | """Check if we have an interactive terminal or not |
Simon Glass | fb35f9f | 2014-09-05 19:00:06 -0600 | [diff] [blame] | 217 | |
Simon Glass | e46c9cf | 2025-04-29 07:22:03 -0600 | [diff] [blame^] | 218 | Returns: |
| 219 | bool: true if an interactive terminal is attached |
| 220 | """ |
| 221 | return os.isatty(sys.stdout.fileno()) |
| 222 | |
| 223 | |
| 224 | class Color(): |
Simon Glass | 381fad8 | 2014-08-28 09:43:34 -0600 | [diff] [blame] | 225 | """Conditionally wraps text in ANSI color escape sequences.""" |
| 226 | BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8) |
| 227 | BOLD = -1 |
Simon Glass | 2027ddd | 2025-04-29 07:22:02 -0600 | [diff] [blame] | 228 | BRIGHT_START = '\033[1;%d%sm' |
| 229 | NORMAL_START = '\033[22;%d%sm' |
Simon Glass | 381fad8 | 2014-08-28 09:43:34 -0600 | [diff] [blame] | 230 | BOLD_START = '\033[1m' |
Simon Glass | 2027ddd | 2025-04-29 07:22:02 -0600 | [diff] [blame] | 231 | BACK_EXTRA = ';%d' |
Simon Glass | 381fad8 | 2014-08-28 09:43:34 -0600 | [diff] [blame] | 232 | RESET = '\033[0m' |
Simon Glass | 2613288 | 2012-01-14 15:12:45 +0000 | [diff] [blame] | 233 | |
Simon Glass | 381fad8 | 2014-08-28 09:43:34 -0600 | [diff] [blame] | 234 | def __init__(self, colored=COLOR_IF_TERMINAL): |
| 235 | """Create a new Color object, optionally disabling color output. |
Simon Glass | 2613288 | 2012-01-14 15:12:45 +0000 | [diff] [blame] | 236 | |
Simon Glass | 381fad8 | 2014-08-28 09:43:34 -0600 | [diff] [blame] | 237 | Args: |
| 238 | enabled: True if color output should be enabled. If False then this |
| 239 | class will not add color codes at all. |
| 240 | """ |
Simon Glass | b0cd341 | 2014-08-28 09:43:35 -0600 | [diff] [blame] | 241 | try: |
| 242 | self._enabled = (colored == COLOR_ALWAYS or |
| 243 | (colored == COLOR_IF_TERMINAL and |
| 244 | os.isatty(sys.stdout.fileno()))) |
| 245 | except: |
| 246 | self._enabled = False |
Simon Glass | 2613288 | 2012-01-14 15:12:45 +0000 | [diff] [blame] | 247 | |
Simon Glass | 2027ddd | 2025-04-29 07:22:02 -0600 | [diff] [blame] | 248 | def enabled(self): |
| 249 | """Check if colour is enabled |
| 250 | |
| 251 | Return: True if enabled, else False |
| 252 | """ |
| 253 | return self._enabled |
| 254 | |
| 255 | def start(self, color, bright=True, back=None): |
Simon Glass | 381fad8 | 2014-08-28 09:43:34 -0600 | [diff] [blame] | 256 | """Returns a start color code. |
Simon Glass | 2613288 | 2012-01-14 15:12:45 +0000 | [diff] [blame] | 257 | |
Simon Glass | 381fad8 | 2014-08-28 09:43:34 -0600 | [diff] [blame] | 258 | Args: |
| 259 | color: Color to use, .e.g BLACK, RED, etc. |
Simon Glass | 2613288 | 2012-01-14 15:12:45 +0000 | [diff] [blame] | 260 | |
Simon Glass | 381fad8 | 2014-08-28 09:43:34 -0600 | [diff] [blame] | 261 | Returns: |
| 262 | If color is enabled, returns an ANSI sequence to start the given |
| 263 | color, otherwise returns empty string |
| 264 | """ |
| 265 | if self._enabled: |
Simon Glass | 2027ddd | 2025-04-29 07:22:02 -0600 | [diff] [blame] | 266 | if color == self.BOLD: |
| 267 | return self.BOLD_START |
Simon Glass | 381fad8 | 2014-08-28 09:43:34 -0600 | [diff] [blame] | 268 | base = self.BRIGHT_START if bright else self.NORMAL_START |
Simon Glass | 2027ddd | 2025-04-29 07:22:02 -0600 | [diff] [blame] | 269 | extra = self.BACK_EXTRA % (back + 40) if back else '' |
| 270 | return base % (color + 30, extra) |
Simon Glass | 381fad8 | 2014-08-28 09:43:34 -0600 | [diff] [blame] | 271 | return '' |
Simon Glass | 2613288 | 2012-01-14 15:12:45 +0000 | [diff] [blame] | 272 | |
Simon Glass | 0281158 | 2022-01-29 14:14:18 -0700 | [diff] [blame] | 273 | def stop(self): |
Anatolij Gustschin | f2bcb32 | 2019-10-27 17:55:04 +0100 | [diff] [blame] | 274 | """Returns a stop color code. |
Simon Glass | 2613288 | 2012-01-14 15:12:45 +0000 | [diff] [blame] | 275 | |
Simon Glass | 381fad8 | 2014-08-28 09:43:34 -0600 | [diff] [blame] | 276 | Returns: |
| 277 | If color is enabled, returns an ANSI color reset sequence, |
| 278 | otherwise returns empty string |
| 279 | """ |
| 280 | if self._enabled: |
| 281 | return self.RESET |
| 282 | return '' |
Simon Glass | 2613288 | 2012-01-14 15:12:45 +0000 | [diff] [blame] | 283 | |
Simon Glass | 2027ddd | 2025-04-29 07:22:02 -0600 | [diff] [blame] | 284 | def build(self, color, text, bright=True, back=None): |
Simon Glass | 381fad8 | 2014-08-28 09:43:34 -0600 | [diff] [blame] | 285 | """Returns text with conditionally added color escape sequences. |
Simon Glass | 2613288 | 2012-01-14 15:12:45 +0000 | [diff] [blame] | 286 | |
Simon Glass | 381fad8 | 2014-08-28 09:43:34 -0600 | [diff] [blame] | 287 | Keyword arguments: |
| 288 | color: Text color -- one of the color constants defined in this |
| 289 | class. |
| 290 | text: The text to color. |
Simon Glass | 2613288 | 2012-01-14 15:12:45 +0000 | [diff] [blame] | 291 | |
Simon Glass | 381fad8 | 2014-08-28 09:43:34 -0600 | [diff] [blame] | 292 | Returns: |
| 293 | If self._enabled is False, returns the original text. If it's True, |
| 294 | returns text with color escape sequences based on the value of |
| 295 | color. |
| 296 | """ |
| 297 | if not self._enabled: |
| 298 | return text |
Simon Glass | 2027ddd | 2025-04-29 07:22:02 -0600 | [diff] [blame] | 299 | return self.start(color, bright, back) + text + self.RESET |
Simon Glass | 14d64e3 | 2025-04-29 07:21:59 -0600 | [diff] [blame] | 300 | |
| 301 | |
| 302 | # Use this to suppress stdout/stderr output: |
| 303 | # with terminal.capture() as (stdout, stderr) |
| 304 | # ...do something... |
| 305 | @contextmanager |
| 306 | def capture(): |
Simon Glass | e884498 | 2025-04-29 07:22:01 -0600 | [diff] [blame] | 307 | global CAPTURING |
| 308 | |
Simon Glass | 14d64e3 | 2025-04-29 07:21:59 -0600 | [diff] [blame] | 309 | capture_out, capture_err = StringIO(), StringIO() |
| 310 | old_out, old_err = sys.stdout, sys.stderr |
| 311 | try: |
Simon Glass | e884498 | 2025-04-29 07:22:01 -0600 | [diff] [blame] | 312 | CAPTURING = True |
Simon Glass | 14d64e3 | 2025-04-29 07:21:59 -0600 | [diff] [blame] | 313 | sys.stdout, sys.stderr = capture_out, capture_err |
| 314 | yield capture_out, capture_err |
| 315 | finally: |
| 316 | sys.stdout, sys.stderr = old_out, old_err |
Simon Glass | e884498 | 2025-04-29 07:22:01 -0600 | [diff] [blame] | 317 | CAPTURING = False |
| 318 | if not USE_CAPTURE: |
| 319 | sys.stdout.write(capture_out.getvalue()) |
| 320 | sys.stderr.write(capture_err.getvalue()) |
Simon Glass | e46c9cf | 2025-04-29 07:22:03 -0600 | [diff] [blame^] | 321 | |
| 322 | |
| 323 | @contextmanager |
| 324 | def pager(): |
| 325 | """Simple pager for outputting lots of text |
| 326 | |
| 327 | Usage: |
| 328 | with terminal.pager(): |
| 329 | print(...) |
| 330 | """ |
| 331 | proc = None |
| 332 | old_stdout = None |
| 333 | try: |
| 334 | less = os.getenv('PAGER') |
| 335 | if not CAPTURING and less != 'none' and have_terminal(): |
| 336 | if not less: |
| 337 | less = 'less -R --quit-if-one-screen' |
| 338 | proc = subprocess.Popen(less, stdin=subprocess.PIPE, text=True, |
| 339 | shell=True) |
| 340 | old_stdout = sys.stdout |
| 341 | sys.stdout = proc.stdin |
| 342 | yield |
| 343 | finally: |
| 344 | if proc: |
| 345 | sys.stdout = old_stdout |
| 346 | proc.communicate() |