Mercurial > repos > shellac > sam_consensus_v3
comparison env/lib/python3.9/site-packages/humanfriendly/terminal/__init__.py @ 0:4f3585e2f14b draft default tip
"planemo upload commit 60cee0fc7c0cda8592644e1aad72851dec82c959"
author | shellac |
---|---|
date | Mon, 22 Mar 2021 18:12:50 +0000 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:4f3585e2f14b |
---|---|
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 ) |