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 )