Mercurial > repos > guerler > springsuite
comparison planemo/lib/python3.7/site-packages/humanfriendly/terminal/__init__.py @ 1:56ad4e20f292 draft
"planemo upload commit 6eee67778febed82ddd413c3ca40b3183a3898f1"
| author | guerler |
|---|---|
| date | Fri, 31 Jul 2020 00:32:28 -0400 |
| parents | |
| children |
comparison
equal
deleted
inserted
replaced
| 0:d30785e31577 | 1:56ad4e20f292 |
|---|---|
| 1 # Human friendly input/output in Python. | |
| 2 # | |
| 3 # Author: Peter Odding <peter@peterodding.com> | |
| 4 # Last Change: March 1, 2020 | |
| 5 # URL: https://humanfriendly.readthedocs.io | |
| 6 | |
| 7 """ | |
| 8 Interaction with interactive text terminals. | |
| 9 | |
| 10 The :mod:`~humanfriendly.terminal` module makes it easy to interact with | |
| 11 interactive text terminals and format text for rendering on such terminals. If | |
| 12 the terms used in the documentation of this module don't make sense to you then | |
| 13 please refer to the `Wikipedia article on ANSI escape sequences`_ for details | |
| 14 about how ANSI escape sequences work. | |
| 15 | |
| 16 This module was originally developed for use on UNIX systems, but since then | |
| 17 Windows 10 gained native support for ANSI escape sequences and this module was | |
| 18 enhanced to recognize and support this. For details please refer to the | |
| 19 :func:`enable_ansi_support()` function. | |
| 20 | |
| 21 .. _Wikipedia article on ANSI escape sequences: http://en.wikipedia.org/wiki/ANSI_escape_code#Sequence_elements | |
| 22 """ | |
| 23 | |
| 24 # Standard library modules. | |
| 25 import codecs | |
| 26 import numbers | |
| 27 import os | |
| 28 import platform | |
| 29 import re | |
| 30 import subprocess | |
| 31 import sys | |
| 32 | |
| 33 # The `fcntl' module is platform specific so importing it may give an error. We | |
| 34 # hide this implementation detail from callers by handling the import error and | |
| 35 # setting a flag instead. | |
| 36 try: | |
| 37 import fcntl | |
| 38 import termios | |
| 39 import struct | |
| 40 HAVE_IOCTL = True | |
| 41 except ImportError: | |
| 42 HAVE_IOCTL = False | |
| 43 | |
| 44 # Modules included in our package. | |
| 45 from humanfriendly.compat import coerce_string, is_unicode, on_windows, which | |
| 46 from humanfriendly.decorators import cached | |
| 47 from humanfriendly.deprecation import define_aliases | |
| 48 from humanfriendly.text import concatenate, format | |
| 49 from humanfriendly.usage import format_usage | |
| 50 | |
| 51 # Public identifiers that require documentation. | |
| 52 __all__ = ( | |
| 53 'ANSI_COLOR_CODES', | |
| 54 'ANSI_CSI', | |
| 55 'ANSI_ERASE_LINE', | |
| 56 'ANSI_HIDE_CURSOR', | |
| 57 'ANSI_RESET', | |
| 58 'ANSI_SGR', | |
| 59 'ANSI_SHOW_CURSOR', | |
| 60 'ANSI_TEXT_STYLES', | |
| 61 'CLEAN_OUTPUT_PATTERN', | |
| 62 'DEFAULT_COLUMNS', | |
| 63 'DEFAULT_ENCODING', | |
| 64 'DEFAULT_LINES', | |
| 65 'HIGHLIGHT_COLOR', | |
| 66 'ansi_strip', | |
| 67 'ansi_style', | |
| 68 'ansi_width', | |
| 69 'ansi_wrap', | |
| 70 'auto_encode', | |
| 71 'clean_terminal_output', | |
| 72 'connected_to_terminal', | |
| 73 'enable_ansi_support', | |
| 74 'find_terminal_size', | |
| 75 'find_terminal_size_using_ioctl', | |
| 76 'find_terminal_size_using_stty', | |
| 77 'get_pager_command', | |
| 78 'have_windows_native_ansi_support', | |
| 79 'message', | |
| 80 'output', | |
| 81 'readline_strip', | |
| 82 'readline_wrap', | |
| 83 'show_pager', | |
| 84 'terminal_supports_colors', | |
| 85 'usage', | |
| 86 'warning', | |
| 87 ) | |
| 88 | |
| 89 ANSI_CSI = '\x1b[' | |
| 90 """The ANSI "Control Sequence Introducer" (a string).""" | |
| 91 | |
| 92 ANSI_SGR = 'm' | |
| 93 """The ANSI "Select Graphic Rendition" sequence (a string).""" | |
| 94 | |
| 95 ANSI_ERASE_LINE = '%sK' % ANSI_CSI | |
| 96 """The ANSI escape sequence to erase the current line (a string).""" | |
| 97 | |
| 98 ANSI_RESET = '%s0%s' % (ANSI_CSI, ANSI_SGR) | |
| 99 """The ANSI escape sequence to reset styling (a string).""" | |
| 100 | |
| 101 ANSI_HIDE_CURSOR = '%s?25l' % ANSI_CSI | |
| 102 """The ANSI escape sequence to hide the text cursor (a string).""" | |
| 103 | |
| 104 ANSI_SHOW_CURSOR = '%s?25h' % ANSI_CSI | |
| 105 """The ANSI escape sequence to show the text cursor (a string).""" | |
| 106 | |
| 107 ANSI_COLOR_CODES = dict(black=0, red=1, green=2, yellow=3, blue=4, magenta=5, cyan=6, white=7) | |
| 108 """ | |
| 109 A dictionary with (name, number) pairs of `portable color codes`_. Used by | |
| 110 :func:`ansi_style()` to generate ANSI escape sequences that change font color. | |
| 111 | |
| 112 .. _portable color codes: http://en.wikipedia.org/wiki/ANSI_escape_code#Colors | |
| 113 """ | |
| 114 | |
| 115 ANSI_TEXT_STYLES = dict(bold=1, faint=2, italic=3, underline=4, inverse=7, strike_through=9) | |
| 116 """ | |
| 117 A dictionary with (name, number) pairs of text styles (effects). Used by | |
| 118 :func:`ansi_style()` to generate ANSI escape sequences that change text | |
| 119 styles. Only widely supported text styles are included here. | |
| 120 """ | |
| 121 | |
| 122 CLEAN_OUTPUT_PATTERN = re.compile(u'(\r|\n|\b|%s)' % re.escape(ANSI_ERASE_LINE)) | |
| 123 """ | |
| 124 A compiled regular expression used to separate significant characters from other text. | |
| 125 | |
| 126 This pattern is used by :func:`clean_terminal_output()` to split terminal | |
| 127 output into regular text versus backspace, carriage return and line feed | |
| 128 characters and ANSI 'erase line' escape sequences. | |
| 129 """ | |
| 130 | |
| 131 DEFAULT_LINES = 25 | |
| 132 """The default number of lines in a terminal (an integer).""" | |
| 133 | |
| 134 DEFAULT_COLUMNS = 80 | |
| 135 """The default number of columns in a terminal (an integer).""" | |
| 136 | |
| 137 DEFAULT_ENCODING = 'UTF-8' | |
| 138 """The output encoding for Unicode strings.""" | |
| 139 | |
| 140 HIGHLIGHT_COLOR = os.environ.get('HUMANFRIENDLY_HIGHLIGHT_COLOR', 'green') | |
| 141 """ | |
| 142 The color used to highlight important tokens in formatted text (e.g. the usage | |
| 143 message of the ``humanfriendly`` program). If the environment variable | |
| 144 ``$HUMANFRIENDLY_HIGHLIGHT_COLOR`` is set it determines the value of | |
| 145 :data:`HIGHLIGHT_COLOR`. | |
| 146 """ | |
| 147 | |
| 148 | |
| 149 def ansi_strip(text, readline_hints=True): | |
| 150 """ | |
| 151 Strip ANSI escape sequences from the given string. | |
| 152 | |
| 153 :param text: The text from which ANSI escape sequences should be removed (a | |
| 154 string). | |
| 155 :param readline_hints: If :data:`True` then :func:`readline_strip()` is | |
| 156 used to remove `readline hints`_ from the string. | |
| 157 :returns: The text without ANSI escape sequences (a string). | |
| 158 """ | |
| 159 pattern = '%s.*?%s' % (re.escape(ANSI_CSI), re.escape(ANSI_SGR)) | |
| 160 text = re.sub(pattern, '', text) | |
| 161 if readline_hints: | |
| 162 text = readline_strip(text) | |
| 163 return text | |
| 164 | |
| 165 | |
| 166 def ansi_style(**kw): | |
| 167 """ | |
| 168 Generate ANSI escape sequences for the given color and/or style(s). | |
| 169 | |
| 170 :param color: The foreground color. Three types of values are supported: | |
| 171 | |
| 172 - The name of a color (one of the strings 'black', 'red', | |
| 173 'green', 'yellow', 'blue', 'magenta', 'cyan' or 'white'). | |
| 174 - An integer that refers to the 256 color mode palette. | |
| 175 - A tuple or list with three integers representing an RGB | |
| 176 (red, green, blue) value. | |
| 177 | |
| 178 The value :data:`None` (the default) means no escape | |
| 179 sequence to switch color will be emitted. | |
| 180 :param background: The background color (see the description | |
| 181 of the `color` argument). | |
| 182 :param bright: Use high intensity colors instead of default colors | |
| 183 (a boolean, defaults to :data:`False`). | |
| 184 :param readline_hints: If :data:`True` then :func:`readline_wrap()` is | |
| 185 applied to the generated ANSI escape sequences (the | |
| 186 default is :data:`False`). | |
| 187 :param kw: Any additional keyword arguments are expected to match a key | |
| 188 in the :data:`ANSI_TEXT_STYLES` dictionary. If the argument's | |
| 189 value evaluates to :data:`True` the respective style will be | |
| 190 enabled. | |
| 191 :returns: The ANSI escape sequences to enable the requested text styles or | |
| 192 an empty string if no styles were requested. | |
| 193 :raises: :exc:`~exceptions.ValueError` when an invalid color name is given. | |
| 194 | |
| 195 Even though only eight named colors are supported, the use of `bright=True` | |
| 196 and `faint=True` increases the number of available colors to around 24 (it | |
| 197 may be slightly lower, for example because faint black is just black). | |
| 198 | |
| 199 **Support for 8-bit colors** | |
| 200 | |
| 201 In `release 4.7`_ support for 256 color mode was added. While this | |
| 202 significantly increases the available colors it's not very human friendly | |
| 203 in usage because you need to look up color codes in the `256 color mode | |
| 204 palette <https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit>`_. | |
| 205 | |
| 206 You can use the ``humanfriendly --demo`` command to get a demonstration of | |
| 207 the available colors, see also the screen shot below. Note that the small | |
| 208 font size in the screen shot was so that the demonstration of 256 color | |
| 209 mode support would fit into a single screen shot without scrolling :-) | |
| 210 (I wasn't feeling very creative). | |
| 211 | |
| 212 .. image:: images/ansi-demo.png | |
| 213 | |
| 214 **Support for 24-bit colors** | |
| 215 | |
| 216 In `release 4.14`_ support for 24-bit colors was added by accepting a tuple | |
| 217 or list with three integers representing the RGB (red, green, blue) value | |
| 218 of a color. This is not included in the demo because rendering millions of | |
| 219 colors was deemed unpractical ;-). | |
| 220 | |
| 221 .. _release 4.7: http://humanfriendly.readthedocs.io/en/latest/changelog.html#release-4-7-2018-01-14 | |
| 222 .. _release 4.14: http://humanfriendly.readthedocs.io/en/latest/changelog.html#release-4-14-2018-07-13 | |
| 223 """ | |
| 224 # Start with sequences that change text styles. | |
| 225 sequences = [ANSI_TEXT_STYLES[k] for k, v in kw.items() if k in ANSI_TEXT_STYLES and v] | |
| 226 # Append the color code (if any). | |
| 227 for color_type in 'color', 'background': | |
| 228 color_value = kw.get(color_type) | |
| 229 if isinstance(color_value, (tuple, list)): | |
| 230 if len(color_value) != 3: | |
| 231 msg = "Invalid color value %r! (expected tuple or list with three numbers)" | |
| 232 raise ValueError(msg % color_value) | |
| 233 sequences.append(48 if color_type == 'background' else 38) | |
| 234 sequences.append(2) | |
| 235 sequences.extend(map(int, color_value)) | |
| 236 elif isinstance(color_value, numbers.Number): | |
| 237 # Numeric values are assumed to be 256 color codes. | |
| 238 sequences.extend(( | |
| 239 39 if color_type == 'background' else 38, | |
| 240 5, int(color_value) | |
| 241 )) | |
| 242 elif color_value: | |
| 243 # Other values are assumed to be strings containing one of the known color names. | |
| 244 if color_value not in ANSI_COLOR_CODES: | |
| 245 msg = "Invalid color value %r! (expected an integer or one of the strings %s)" | |
| 246 raise ValueError(msg % (color_value, concatenate(map(repr, sorted(ANSI_COLOR_CODES))))) | |
| 247 # Pick the right offset for foreground versus background | |
| 248 # colors and regular intensity versus bright colors. | |
| 249 offset = ( | |
| 250 (100 if kw.get('bright') else 40) | |
| 251 if color_type == 'background' | |
| 252 else (90 if kw.get('bright') else 30) | |
| 253 ) | |
| 254 # Combine the offset and color code into a single integer. | |
| 255 sequences.append(offset + ANSI_COLOR_CODES[color_value]) | |
| 256 if sequences: | |
| 257 encoded = ANSI_CSI + ';'.join(map(str, sequences)) + ANSI_SGR | |
| 258 return readline_wrap(encoded) if kw.get('readline_hints') else encoded | |
| 259 else: | |
| 260 return '' | |
| 261 | |
| 262 | |
| 263 def ansi_width(text): | |
| 264 """ | |
| 265 Calculate the effective width of the given text (ignoring ANSI escape sequences). | |
| 266 | |
| 267 :param text: The text whose width should be calculated (a string). | |
| 268 :returns: The width of the text without ANSI escape sequences (an | |
| 269 integer). | |
| 270 | |
| 271 This function uses :func:`ansi_strip()` to strip ANSI escape sequences from | |
| 272 the given string and returns the length of the resulting string. | |
| 273 """ | |
| 274 return len(ansi_strip(text)) | |
| 275 | |
| 276 | |
| 277 def ansi_wrap(text, **kw): | |
| 278 """ | |
| 279 Wrap text in ANSI escape sequences for the given color and/or style(s). | |
| 280 | |
| 281 :param text: The text to wrap (a string). | |
| 282 :param kw: Any keyword arguments are passed to :func:`ansi_style()`. | |
| 283 :returns: The result of this function depends on the keyword arguments: | |
| 284 | |
| 285 - If :func:`ansi_style()` generates an ANSI escape sequence based | |
| 286 on the keyword arguments, the given text is prefixed with the | |
| 287 generated ANSI escape sequence and suffixed with | |
| 288 :data:`ANSI_RESET`. | |
| 289 | |
| 290 - If :func:`ansi_style()` returns an empty string then the text | |
| 291 given by the caller is returned unchanged. | |
| 292 """ | |
| 293 start_sequence = ansi_style(**kw) | |
| 294 if start_sequence: | |
| 295 end_sequence = ANSI_RESET | |
| 296 if kw.get('readline_hints'): | |
| 297 end_sequence = readline_wrap(end_sequence) | |
| 298 return start_sequence + text + end_sequence | |
| 299 else: | |
| 300 return text | |
| 301 | |
| 302 | |
| 303 def auto_encode(stream, text, *args, **kw): | |
| 304 """ | |
| 305 Reliably write Unicode strings to the terminal. | |
| 306 | |
| 307 :param stream: The file-like object to write to (a value like | |
| 308 :data:`sys.stdout` or :data:`sys.stderr`). | |
| 309 :param text: The text to write to the stream (a string). | |
| 310 :param args: Refer to :func:`~humanfriendly.text.format()`. | |
| 311 :param kw: Refer to :func:`~humanfriendly.text.format()`. | |
| 312 | |
| 313 Renders the text using :func:`~humanfriendly.text.format()` and writes it | |
| 314 to the given stream. If an :exc:`~exceptions.UnicodeEncodeError` is | |
| 315 encountered in doing so, the text is encoded using :data:`DEFAULT_ENCODING` | |
| 316 and the write is retried. The reasoning behind this rather blunt approach | |
| 317 is that it's preferable to get output on the command line in the wrong | |
| 318 encoding then to have the Python program blow up with a | |
| 319 :exc:`~exceptions.UnicodeEncodeError` exception. | |
| 320 """ | |
| 321 text = format(text, *args, **kw) | |
| 322 try: | |
| 323 stream.write(text) | |
| 324 except UnicodeEncodeError: | |
| 325 stream.write(codecs.encode(text, DEFAULT_ENCODING)) | |
| 326 | |
| 327 | |
| 328 def clean_terminal_output(text): | |
| 329 """ | |
| 330 Clean up the terminal output of a command. | |
| 331 | |
| 332 :param text: The raw text with special characters (a Unicode string). | |
| 333 :returns: A list of Unicode strings (one for each line). | |
| 334 | |
| 335 This function emulates the effect of backspace (0x08), carriage return | |
| 336 (0x0D) and line feed (0x0A) characters and the ANSI 'erase line' escape | |
| 337 sequence on interactive terminals. It's intended to clean up command output | |
| 338 that was originally meant to be rendered on an interactive terminal and | |
| 339 that has been captured using e.g. the :man:`script` program [#]_ or the | |
| 340 :mod:`pty` module [#]_. | |
| 341 | |
| 342 .. [#] My coloredlogs_ package supports the ``coloredlogs --to-html`` | |
| 343 command which uses :man:`script` to fool a subprocess into thinking | |
| 344 that it's connected to an interactive terminal (in order to get it | |
| 345 to emit ANSI escape sequences). | |
| 346 | |
| 347 .. [#] My capturer_ package uses the :mod:`pty` module to fool the current | |
| 348 process and subprocesses into thinking they are connected to an | |
| 349 interactive terminal (in order to get them to emit ANSI escape | |
| 350 sequences). | |
| 351 | |
| 352 **Some caveats about the use of this function:** | |
| 353 | |
| 354 - Strictly speaking the effect of carriage returns cannot be emulated | |
| 355 outside of an actual terminal due to the interaction between overlapping | |
| 356 output, terminal widths and line wrapping. The goal of this function is | |
| 357 to sanitize noise in terminal output while preserving useful output. | |
| 358 Think of it as a useful and pragmatic but possibly lossy conversion. | |
| 359 | |
| 360 - The algorithm isn't smart enough to properly handle a pair of ANSI escape | |
| 361 sequences that open before a carriage return and close after the last | |
| 362 carriage return in a linefeed delimited string; the resulting string will | |
| 363 contain only the closing end of the ANSI escape sequence pair. Tracking | |
| 364 this kind of complexity requires a state machine and proper parsing. | |
| 365 | |
| 366 .. _capturer: https://pypi.org/project/capturer | |
| 367 .. _coloredlogs: https://pypi.org/project/coloredlogs | |
| 368 """ | |
| 369 cleaned_lines = [] | |
| 370 current_line = '' | |
| 371 current_position = 0 | |
| 372 for token in CLEAN_OUTPUT_PATTERN.split(text): | |
| 373 if token == '\r': | |
| 374 # Seek back to the start of the current line. | |
| 375 current_position = 0 | |
| 376 elif token == '\b': | |
| 377 # Seek back one character in the current line. | |
| 378 current_position = max(0, current_position - 1) | |
| 379 else: | |
| 380 if token == '\n': | |
| 381 # Capture the current line. | |
| 382 cleaned_lines.append(current_line) | |
| 383 if token in ('\n', ANSI_ERASE_LINE): | |
| 384 # Clear the current line. | |
| 385 current_line = '' | |
| 386 current_position = 0 | |
| 387 elif token: | |
| 388 # Merge regular output into the current line. | |
| 389 new_position = current_position + len(token) | |
| 390 prefix = current_line[:current_position] | |
| 391 suffix = current_line[new_position:] | |
| 392 current_line = prefix + token + suffix | |
| 393 current_position = new_position | |
| 394 # Capture the last line (if any). | |
| 395 cleaned_lines.append(current_line) | |
| 396 # Remove any empty trailing lines. | |
| 397 while cleaned_lines and not cleaned_lines[-1]: | |
| 398 cleaned_lines.pop(-1) | |
| 399 return cleaned_lines | |
| 400 | |
| 401 | |
| 402 def connected_to_terminal(stream=None): | |
| 403 """ | |
| 404 Check if a stream is connected to a terminal. | |
| 405 | |
| 406 :param stream: The stream to check (a file-like object, | |
| 407 defaults to :data:`sys.stdout`). | |
| 408 :returns: :data:`True` if the stream is connected to a terminal, | |
| 409 :data:`False` otherwise. | |
| 410 | |
| 411 See also :func:`terminal_supports_colors()`. | |
| 412 """ | |
| 413 stream = sys.stdout if stream is None else stream | |
| 414 try: | |
| 415 return stream.isatty() | |
| 416 except Exception: | |
| 417 return False | |
| 418 | |
| 419 | |
| 420 @cached | |
| 421 def enable_ansi_support(): | |
| 422 """ | |
| 423 Try to enable support for ANSI escape sequences (required on Windows). | |
| 424 | |
| 425 :returns: :data:`True` if ANSI is supported, :data:`False` otherwise. | |
| 426 | |
| 427 This functions checks for the following supported configurations, in the | |
| 428 given order: | |
| 429 | |
| 430 1. On Windows, if :func:`have_windows_native_ansi_support()` confirms | |
| 431 native support for ANSI escape sequences :mod:`ctypes` will be used to | |
| 432 enable this support. | |
| 433 | |
| 434 2. On Windows, if the environment variable ``$ANSICON`` is set nothing is | |
| 435 done because it is assumed that support for ANSI escape sequences has | |
| 436 already been enabled via `ansicon <https://github.com/adoxa/ansicon>`_. | |
| 437 | |
| 438 3. On Windows, an attempt is made to import and initialize the Python | |
| 439 package :pypi:`colorama` instead (of course for this to work | |
| 440 :pypi:`colorama` has to be installed). | |
| 441 | |
| 442 4. On other platforms this function calls :func:`connected_to_terminal()` | |
| 443 to determine whether ANSI escape sequences are supported (that is to | |
| 444 say all platforms that are not Windows are assumed to support ANSI | |
| 445 escape sequences natively, without weird contortions like above). | |
| 446 | |
| 447 This makes it possible to call :func:`enable_ansi_support()` | |
| 448 unconditionally without checking the current platform. | |
| 449 | |
| 450 The :func:`~humanfriendly.decorators.cached` decorator is used to ensure | |
| 451 that this function is only executed once, but its return value remains | |
| 452 available on later calls. | |
| 453 """ | |
| 454 if have_windows_native_ansi_support(): | |
| 455 import ctypes | |
| 456 ctypes.windll.kernel32.SetConsoleMode(ctypes.windll.kernel32.GetStdHandle(-11), 7) | |
| 457 ctypes.windll.kernel32.SetConsoleMode(ctypes.windll.kernel32.GetStdHandle(-12), 7) | |
| 458 return True | |
| 459 elif on_windows(): | |
| 460 if 'ANSICON' in os.environ: | |
| 461 return True | |
| 462 try: | |
| 463 import colorama | |
| 464 colorama.init() | |
| 465 return True | |
| 466 except ImportError: | |
| 467 return False | |
| 468 else: | |
| 469 return connected_to_terminal() | |
| 470 | |
| 471 | |
| 472 def find_terminal_size(): | |
| 473 """ | |
| 474 Determine the number of lines and columns visible in the terminal. | |
| 475 | |
| 476 :returns: A tuple of two integers with the line and column count. | |
| 477 | |
| 478 The result of this function is based on the first of the following three | |
| 479 methods that works: | |
| 480 | |
| 481 1. First :func:`find_terminal_size_using_ioctl()` is tried, | |
| 482 2. then :func:`find_terminal_size_using_stty()` is tried, | |
| 483 3. finally :data:`DEFAULT_LINES` and :data:`DEFAULT_COLUMNS` are returned. | |
| 484 | |
| 485 .. note:: The :func:`find_terminal_size()` function performs the steps | |
| 486 above every time it is called, the result is not cached. This is | |
| 487 because the size of a virtual terminal can change at any time and | |
| 488 the result of :func:`find_terminal_size()` should be correct. | |
| 489 | |
| 490 `Pre-emptive snarky comment`_: It's possible to cache the result | |
| 491 of this function and use :mod:`signal.SIGWINCH <signal>` to | |
| 492 refresh the cached values! | |
| 493 | |
| 494 Response: As a library I don't consider it the role of the | |
| 495 :mod:`humanfriendly.terminal` module to install a process wide | |
| 496 signal handler ... | |
| 497 | |
| 498 .. _Pre-emptive snarky comment: http://blogs.msdn.com/b/oldnewthing/archive/2008/01/30/7315957.aspx | |
| 499 """ | |
| 500 # The first method. Any of the standard streams may have been redirected | |
| 501 # somewhere and there's no telling which, so we'll just try them all. | |
| 502 for stream in sys.stdin, sys.stdout, sys.stderr: | |
| 503 try: | |
| 504 result = find_terminal_size_using_ioctl(stream) | |
| 505 if min(result) >= 1: | |
| 506 return result | |
| 507 except Exception: | |
| 508 pass | |
| 509 # The second method. | |
| 510 try: | |
| 511 result = find_terminal_size_using_stty() | |
| 512 if min(result) >= 1: | |
| 513 return result | |
| 514 except Exception: | |
| 515 pass | |
| 516 # Fall back to conservative defaults. | |
| 517 return DEFAULT_LINES, DEFAULT_COLUMNS | |
| 518 | |
| 519 | |
| 520 def find_terminal_size_using_ioctl(stream): | |
| 521 """ | |
| 522 Find the terminal size using :func:`fcntl.ioctl()`. | |
| 523 | |
| 524 :param stream: A stream connected to the terminal (a file object with a | |
| 525 ``fileno`` attribute). | |
| 526 :returns: A tuple of two integers with the line and column count. | |
| 527 :raises: This function can raise exceptions but I'm not going to document | |
| 528 them here, you should be using :func:`find_terminal_size()`. | |
| 529 | |
| 530 Based on an `implementation found on StackOverflow <http://stackoverflow.com/a/3010495/788200>`_. | |
| 531 """ | |
| 532 if not HAVE_IOCTL: | |
| 533 raise NotImplementedError("It looks like the `fcntl' module is not available!") | |
| 534 h, w, hp, wp = struct.unpack('HHHH', fcntl.ioctl(stream, termios.TIOCGWINSZ, struct.pack('HHHH', 0, 0, 0, 0))) | |
| 535 return h, w | |
| 536 | |
| 537 | |
| 538 def find_terminal_size_using_stty(): | |
| 539 """ | |
| 540 Find the terminal size using the external command ``stty size``. | |
| 541 | |
| 542 :param stream: A stream connected to the terminal (a file object). | |
| 543 :returns: A tuple of two integers with the line and column count. | |
| 544 :raises: This function can raise exceptions but I'm not going to document | |
| 545 them here, you should be using :func:`find_terminal_size()`. | |
| 546 """ | |
| 547 stty = subprocess.Popen(['stty', 'size'], | |
| 548 stdout=subprocess.PIPE, | |
| 549 stderr=subprocess.PIPE) | |
| 550 stdout, stderr = stty.communicate() | |
| 551 tokens = stdout.split() | |
| 552 if len(tokens) != 2: | |
| 553 raise Exception("Invalid output from `stty size'!") | |
| 554 return tuple(map(int, tokens)) | |
| 555 | |
| 556 | |
| 557 def get_pager_command(text=None): | |
| 558 """ | |
| 559 Get the command to show a text on the terminal using a pager. | |
| 560 | |
| 561 :param text: The text to print to the terminal (a string). | |
| 562 :returns: A list of strings with the pager command and arguments. | |
| 563 | |
| 564 The use of a pager helps to avoid the wall of text effect where the user | |
| 565 has to scroll up to see where the output began (not very user friendly). | |
| 566 | |
| 567 If the given text contains ANSI escape sequences the command ``less | |
| 568 --RAW-CONTROL-CHARS`` is used, otherwise the environment variable | |
| 569 ``$PAGER`` is used (if ``$PAGER`` isn't set :man:`less` is used). | |
| 570 | |
| 571 When the selected pager is :man:`less`, the following options are used to | |
| 572 make the experience more user friendly: | |
| 573 | |
| 574 - ``--quit-if-one-screen`` causes :man:`less` to automatically exit if the | |
| 575 entire text can be displayed on the first screen. This makes the use of a | |
| 576 pager transparent for smaller texts (because the operator doesn't have to | |
| 577 quit the pager). | |
| 578 | |
| 579 - ``--no-init`` prevents :man:`less` from clearing the screen when it | |
| 580 exits. This ensures that the operator gets a chance to review the text | |
| 581 (for example a usage message) after quitting the pager, while composing | |
| 582 the next command. | |
| 583 """ | |
| 584 # Compose the pager command. | |
| 585 if text and ANSI_CSI in text: | |
| 586 command_line = ['less', '--RAW-CONTROL-CHARS'] | |
| 587 else: | |
| 588 command_line = [os.environ.get('PAGER', 'less')] | |
| 589 # Pass some additional options to `less' (to make it more | |
| 590 # user friendly) without breaking support for other pagers. | |
| 591 if os.path.basename(command_line[0]) == 'less': | |
| 592 command_line.append('--no-init') | |
| 593 command_line.append('--quit-if-one-screen') | |
| 594 return command_line | |
| 595 | |
| 596 | |
| 597 @cached | |
| 598 def have_windows_native_ansi_support(): | |
| 599 """ | |
| 600 Check if we're running on a Windows 10 release with native support for ANSI escape sequences. | |
| 601 | |
| 602 :returns: :data:`True` if so, :data:`False` otherwise. | |
| 603 | |
| 604 The :func:`~humanfriendly.decorators.cached` decorator is used as a minor | |
| 605 performance optimization. Semantically this should have zero impact because | |
| 606 the answer doesn't change in the lifetime of a computer process. | |
| 607 """ | |
| 608 if on_windows(): | |
| 609 try: | |
| 610 # I can't be 100% sure this will never break and I'm not in a | |
| 611 # position to test it thoroughly either, so I decided that paying | |
| 612 # the price of one additional try / except statement is worth the | |
| 613 # additional peace of mind :-). | |
| 614 components = tuple(int(c) for c in platform.version().split('.')) | |
| 615 return components >= (10, 0, 14393) | |
| 616 except Exception: | |
| 617 pass | |
| 618 return False | |
| 619 | |
| 620 | |
| 621 def message(text, *args, **kw): | |
| 622 """ | |
| 623 Print a formatted message to the standard error stream. | |
| 624 | |
| 625 For details about argument handling please refer to | |
| 626 :func:`~humanfriendly.text.format()`. | |
| 627 | |
| 628 Renders the message using :func:`~humanfriendly.text.format()` and writes | |
| 629 the resulting string (followed by a newline) to :data:`sys.stderr` using | |
| 630 :func:`auto_encode()`. | |
| 631 """ | |
| 632 auto_encode(sys.stderr, coerce_string(text) + '\n', *args, **kw) | |
| 633 | |
| 634 | |
| 635 def output(text, *args, **kw): | |
| 636 """ | |
| 637 Print a formatted message to the standard output stream. | |
| 638 | |
| 639 For details about argument handling please refer to | |
| 640 :func:`~humanfriendly.text.format()`. | |
| 641 | |
| 642 Renders the message using :func:`~humanfriendly.text.format()` and writes | |
| 643 the resulting string (followed by a newline) to :data:`sys.stdout` using | |
| 644 :func:`auto_encode()`. | |
| 645 """ | |
| 646 auto_encode(sys.stdout, coerce_string(text) + '\n', *args, **kw) | |
| 647 | |
| 648 | |
| 649 def readline_strip(expr): | |
| 650 """ | |
| 651 Remove `readline hints`_ from a string. | |
| 652 | |
| 653 :param text: The text to strip (a string). | |
| 654 :returns: The stripped text. | |
| 655 """ | |
| 656 return expr.replace('\001', '').replace('\002', '') | |
| 657 | |
| 658 | |
| 659 def readline_wrap(expr): | |
| 660 """ | |
| 661 Wrap an ANSI escape sequence in `readline hints`_. | |
| 662 | |
| 663 :param text: The text with the escape sequence to wrap (a string). | |
| 664 :returns: The wrapped text. | |
| 665 | |
| 666 .. _readline hints: http://superuser.com/a/301355 | |
| 667 """ | |
| 668 return '\001' + expr + '\002' | |
| 669 | |
| 670 | |
| 671 def show_pager(formatted_text, encoding=DEFAULT_ENCODING): | |
| 672 """ | |
| 673 Print a large text to the terminal using a pager. | |
| 674 | |
| 675 :param formatted_text: The text to print to the terminal (a string). | |
| 676 :param encoding: The name of the text encoding used to encode the formatted | |
| 677 text if the formatted text is a Unicode string (a string, | |
| 678 defaults to :data:`DEFAULT_ENCODING`). | |
| 679 | |
| 680 When :func:`connected_to_terminal()` returns :data:`True` a pager is used | |
| 681 to show the text on the terminal, otherwise the text is printed directly | |
| 682 without invoking a pager. | |
| 683 | |
| 684 The use of a pager helps to avoid the wall of text effect where the user | |
| 685 has to scroll up to see where the output began (not very user friendly). | |
| 686 | |
| 687 Refer to :func:`get_pager_command()` for details about the command line | |
| 688 that's used to invoke the pager. | |
| 689 """ | |
| 690 if connected_to_terminal(): | |
| 691 # Make sure the selected pager command is available. | |
| 692 command_line = get_pager_command(formatted_text) | |
| 693 if which(command_line[0]): | |
| 694 pager = subprocess.Popen(command_line, stdin=subprocess.PIPE) | |
| 695 if is_unicode(formatted_text): | |
| 696 formatted_text = formatted_text.encode(encoding) | |
| 697 pager.communicate(input=formatted_text) | |
| 698 return | |
| 699 output(formatted_text) | |
| 700 | |
| 701 | |
| 702 def terminal_supports_colors(stream=None): | |
| 703 """ | |
| 704 Check if a stream is connected to a terminal that supports ANSI escape sequences. | |
| 705 | |
| 706 :param stream: The stream to check (a file-like object, | |
| 707 defaults to :data:`sys.stdout`). | |
| 708 :returns: :data:`True` if the terminal supports ANSI escape sequences, | |
| 709 :data:`False` otherwise. | |
| 710 | |
| 711 This function was originally inspired by the implementation of | |
| 712 `django.core.management.color.supports_color() | |
| 713 <https://github.com/django/django/blob/master/django/core/management/color.py>`_ | |
| 714 but has since evolved significantly. | |
| 715 """ | |
| 716 if on_windows(): | |
| 717 # On Windows support for ANSI escape sequences is not a given. | |
| 718 have_ansicon = 'ANSICON' in os.environ | |
| 719 have_colorama = 'colorama' in sys.modules | |
| 720 have_native_support = have_windows_native_ansi_support() | |
| 721 if not (have_ansicon or have_colorama or have_native_support): | |
| 722 return False | |
| 723 return connected_to_terminal(stream) | |
| 724 | |
| 725 | |
| 726 def usage(usage_text): | |
| 727 """ | |
| 728 Print a human friendly usage message to the terminal. | |
| 729 | |
| 730 :param text: The usage message to print (a string). | |
| 731 | |
| 732 This function does two things: | |
| 733 | |
| 734 1. If :data:`sys.stdout` is connected to a terminal (see | |
| 735 :func:`connected_to_terminal()`) then the usage message is formatted | |
| 736 using :func:`.format_usage()`. | |
| 737 2. The usage message is shown using a pager (see :func:`show_pager()`). | |
| 738 """ | |
| 739 if terminal_supports_colors(sys.stdout): | |
| 740 usage_text = format_usage(usage_text) | |
| 741 show_pager(usage_text) | |
| 742 | |
| 743 | |
| 744 def warning(text, *args, **kw): | |
| 745 """ | |
| 746 Show a warning message on the terminal. | |
| 747 | |
| 748 For details about argument handling please refer to | |
| 749 :func:`~humanfriendly.text.format()`. | |
| 750 | |
| 751 Renders the message using :func:`~humanfriendly.text.format()` and writes | |
| 752 the resulting string (followed by a newline) to :data:`sys.stderr` using | |
| 753 :func:`auto_encode()`. | |
| 754 | |
| 755 If :data:`sys.stderr` is connected to a terminal that supports colors, | |
| 756 :func:`ansi_wrap()` is used to color the message in a red font (to make | |
| 757 the warning stand out from surrounding text). | |
| 758 """ | |
| 759 text = coerce_string(text) | |
| 760 if terminal_supports_colors(sys.stderr): | |
| 761 text = ansi_wrap(text, color='red') | |
| 762 auto_encode(sys.stderr, text + '\n', *args, **kw) | |
| 763 | |
| 764 | |
| 765 # Define aliases for backwards compatibility. | |
| 766 define_aliases( | |
| 767 module_name=__name__, | |
| 768 # In humanfriendly 1.31 the find_meta_variables() and format_usage() | |
| 769 # functions were extracted to the new module humanfriendly.usage. | |
| 770 find_meta_variables='humanfriendly.usage.find_meta_variables', | |
| 771 format_usage='humanfriendly.usage.format_usage', | |
| 772 # In humanfriendly 8.0 the html_to_ansi() function and HTMLConverter | |
| 773 # class were extracted to the new module humanfriendly.terminal.html. | |
| 774 html_to_ansi='humanfriendly.terminal.html.html_to_ansi', | |
| 775 HTMLConverter='humanfriendly.terminal.html.HTMLConverter', | |
| 776 ) |
