| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776 |
- # Human friendly input/output in Python.
- #
- # Author: Peter Odding <peter@peterodding.com>
- # Last Change: March 1, 2020
- # URL: https://humanfriendly.readthedocs.io
- """
- Interaction with interactive text terminals.
- The :mod:`~humanfriendly.terminal` module makes it easy to interact with
- interactive text terminals and format text for rendering on such terminals. If
- the terms used in the documentation of this module don't make sense to you then
- please refer to the `Wikipedia article on ANSI escape sequences`_ for details
- about how ANSI escape sequences work.
- This module was originally developed for use on UNIX systems, but since then
- Windows 10 gained native support for ANSI escape sequences and this module was
- enhanced to recognize and support this. For details please refer to the
- :func:`enable_ansi_support()` function.
- .. _Wikipedia article on ANSI escape sequences: http://en.wikipedia.org/wiki/ANSI_escape_code#Sequence_elements
- """
- # Standard library modules.
- import codecs
- import numbers
- import os
- import platform
- import re
- import subprocess
- import sys
- # The `fcntl' module is platform specific so importing it may give an error. We
- # hide this implementation detail from callers by handling the import error and
- # setting a flag instead.
- try:
- import fcntl
- import termios
- import struct
- HAVE_IOCTL = True
- except ImportError:
- HAVE_IOCTL = False
- # Modules included in our package.
- from humanfriendly.compat import coerce_string, is_unicode, on_windows, which
- from humanfriendly.decorators import cached
- from humanfriendly.deprecation import define_aliases
- from humanfriendly.text import concatenate, format
- from humanfriendly.usage import format_usage
- # Public identifiers that require documentation.
- __all__ = (
- 'ANSI_COLOR_CODES',
- 'ANSI_CSI',
- 'ANSI_ERASE_LINE',
- 'ANSI_HIDE_CURSOR',
- 'ANSI_RESET',
- 'ANSI_SGR',
- 'ANSI_SHOW_CURSOR',
- 'ANSI_TEXT_STYLES',
- 'CLEAN_OUTPUT_PATTERN',
- 'DEFAULT_COLUMNS',
- 'DEFAULT_ENCODING',
- 'DEFAULT_LINES',
- 'HIGHLIGHT_COLOR',
- 'ansi_strip',
- 'ansi_style',
- 'ansi_width',
- 'ansi_wrap',
- 'auto_encode',
- 'clean_terminal_output',
- 'connected_to_terminal',
- 'enable_ansi_support',
- 'find_terminal_size',
- 'find_terminal_size_using_ioctl',
- 'find_terminal_size_using_stty',
- 'get_pager_command',
- 'have_windows_native_ansi_support',
- 'message',
- 'output',
- 'readline_strip',
- 'readline_wrap',
- 'show_pager',
- 'terminal_supports_colors',
- 'usage',
- 'warning',
- )
- ANSI_CSI = '\x1b['
- """The ANSI "Control Sequence Introducer" (a string)."""
- ANSI_SGR = 'm'
- """The ANSI "Select Graphic Rendition" sequence (a string)."""
- ANSI_ERASE_LINE = '%sK' % ANSI_CSI
- """The ANSI escape sequence to erase the current line (a string)."""
- ANSI_RESET = '%s0%s' % (ANSI_CSI, ANSI_SGR)
- """The ANSI escape sequence to reset styling (a string)."""
- ANSI_HIDE_CURSOR = '%s?25l' % ANSI_CSI
- """The ANSI escape sequence to hide the text cursor (a string)."""
- ANSI_SHOW_CURSOR = '%s?25h' % ANSI_CSI
- """The ANSI escape sequence to show the text cursor (a string)."""
- ANSI_COLOR_CODES = dict(black=0, red=1, green=2, yellow=3, blue=4, magenta=5, cyan=6, white=7)
- """
- A dictionary with (name, number) pairs of `portable color codes`_. Used by
- :func:`ansi_style()` to generate ANSI escape sequences that change font color.
- .. _portable color codes: http://en.wikipedia.org/wiki/ANSI_escape_code#Colors
- """
- ANSI_TEXT_STYLES = dict(bold=1, faint=2, italic=3, underline=4, inverse=7, strike_through=9)
- """
- A dictionary with (name, number) pairs of text styles (effects). Used by
- :func:`ansi_style()` to generate ANSI escape sequences that change text
- styles. Only widely supported text styles are included here.
- """
- CLEAN_OUTPUT_PATTERN = re.compile(u'(\r|\n|\b|%s)' % re.escape(ANSI_ERASE_LINE))
- """
- A compiled regular expression used to separate significant characters from other text.
- This pattern is used by :func:`clean_terminal_output()` to split terminal
- output into regular text versus backspace, carriage return and line feed
- characters and ANSI 'erase line' escape sequences.
- """
- DEFAULT_LINES = 25
- """The default number of lines in a terminal (an integer)."""
- DEFAULT_COLUMNS = 80
- """The default number of columns in a terminal (an integer)."""
- DEFAULT_ENCODING = 'UTF-8'
- """The output encoding for Unicode strings."""
- HIGHLIGHT_COLOR = os.environ.get('HUMANFRIENDLY_HIGHLIGHT_COLOR', 'green')
- """
- The color used to highlight important tokens in formatted text (e.g. the usage
- message of the ``humanfriendly`` program). If the environment variable
- ``$HUMANFRIENDLY_HIGHLIGHT_COLOR`` is set it determines the value of
- :data:`HIGHLIGHT_COLOR`.
- """
- def ansi_strip(text, readline_hints=True):
- """
- Strip ANSI escape sequences from the given string.
- :param text: The text from which ANSI escape sequences should be removed (a
- string).
- :param readline_hints: If :data:`True` then :func:`readline_strip()` is
- used to remove `readline hints`_ from the string.
- :returns: The text without ANSI escape sequences (a string).
- """
- pattern = '%s.*?%s' % (re.escape(ANSI_CSI), re.escape(ANSI_SGR))
- text = re.sub(pattern, '', text)
- if readline_hints:
- text = readline_strip(text)
- return text
- def ansi_style(**kw):
- """
- Generate ANSI escape sequences for the given color and/or style(s).
- :param color: The foreground color. Three types of values are supported:
- - The name of a color (one of the strings 'black', 'red',
- 'green', 'yellow', 'blue', 'magenta', 'cyan' or 'white').
- - An integer that refers to the 256 color mode palette.
- - A tuple or list with three integers representing an RGB
- (red, green, blue) value.
- The value :data:`None` (the default) means no escape
- sequence to switch color will be emitted.
- :param background: The background color (see the description
- of the `color` argument).
- :param bright: Use high intensity colors instead of default colors
- (a boolean, defaults to :data:`False`).
- :param readline_hints: If :data:`True` then :func:`readline_wrap()` is
- applied to the generated ANSI escape sequences (the
- default is :data:`False`).
- :param kw: Any additional keyword arguments are expected to match a key
- in the :data:`ANSI_TEXT_STYLES` dictionary. If the argument's
- value evaluates to :data:`True` the respective style will be
- enabled.
- :returns: The ANSI escape sequences to enable the requested text styles or
- an empty string if no styles were requested.
- :raises: :exc:`~exceptions.ValueError` when an invalid color name is given.
- Even though only eight named colors are supported, the use of `bright=True`
- and `faint=True` increases the number of available colors to around 24 (it
- may be slightly lower, for example because faint black is just black).
- **Support for 8-bit colors**
- In `release 4.7`_ support for 256 color mode was added. While this
- significantly increases the available colors it's not very human friendly
- in usage because you need to look up color codes in the `256 color mode
- palette <https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit>`_.
- You can use the ``humanfriendly --demo`` command to get a demonstration of
- the available colors, see also the screen shot below. Note that the small
- font size in the screen shot was so that the demonstration of 256 color
- mode support would fit into a single screen shot without scrolling :-)
- (I wasn't feeling very creative).
- .. image:: images/ansi-demo.png
- **Support for 24-bit colors**
- In `release 4.14`_ support for 24-bit colors was added by accepting a tuple
- or list with three integers representing the RGB (red, green, blue) value
- of a color. This is not included in the demo because rendering millions of
- colors was deemed unpractical ;-).
- .. _release 4.7: http://humanfriendly.readthedocs.io/en/latest/changelog.html#release-4-7-2018-01-14
- .. _release 4.14: http://humanfriendly.readthedocs.io/en/latest/changelog.html#release-4-14-2018-07-13
- """
- # Start with sequences that change text styles.
- sequences = [ANSI_TEXT_STYLES[k] for k, v in kw.items() if k in ANSI_TEXT_STYLES and v]
- # Append the color code (if any).
- for color_type in 'color', 'background':
- color_value = kw.get(color_type)
- if isinstance(color_value, (tuple, list)):
- if len(color_value) != 3:
- msg = "Invalid color value %r! (expected tuple or list with three numbers)"
- raise ValueError(msg % color_value)
- sequences.append(48 if color_type == 'background' else 38)
- sequences.append(2)
- sequences.extend(map(int, color_value))
- elif isinstance(color_value, numbers.Number):
- # Numeric values are assumed to be 256 color codes.
- sequences.extend((
- 39 if color_type == 'background' else 38,
- 5, int(color_value)
- ))
- elif color_value:
- # Other values are assumed to be strings containing one of the known color names.
- if color_value not in ANSI_COLOR_CODES:
- msg = "Invalid color value %r! (expected an integer or one of the strings %s)"
- raise ValueError(msg % (color_value, concatenate(map(repr, sorted(ANSI_COLOR_CODES)))))
- # Pick the right offset for foreground versus background
- # colors and regular intensity versus bright colors.
- offset = (
- (100 if kw.get('bright') else 40)
- if color_type == 'background'
- else (90 if kw.get('bright') else 30)
- )
- # Combine the offset and color code into a single integer.
- sequences.append(offset + ANSI_COLOR_CODES[color_value])
- if sequences:
- encoded = ANSI_CSI + ';'.join(map(str, sequences)) + ANSI_SGR
- return readline_wrap(encoded) if kw.get('readline_hints') else encoded
- else:
- return ''
- def ansi_width(text):
- """
- Calculate the effective width of the given text (ignoring ANSI escape sequences).
- :param text: The text whose width should be calculated (a string).
- :returns: The width of the text without ANSI escape sequences (an
- integer).
- This function uses :func:`ansi_strip()` to strip ANSI escape sequences from
- the given string and returns the length of the resulting string.
- """
- return len(ansi_strip(text))
- def ansi_wrap(text, **kw):
- """
- Wrap text in ANSI escape sequences for the given color and/or style(s).
- :param text: The text to wrap (a string).
- :param kw: Any keyword arguments are passed to :func:`ansi_style()`.
- :returns: The result of this function depends on the keyword arguments:
- - If :func:`ansi_style()` generates an ANSI escape sequence based
- on the keyword arguments, the given text is prefixed with the
- generated ANSI escape sequence and suffixed with
- :data:`ANSI_RESET`.
- - If :func:`ansi_style()` returns an empty string then the text
- given by the caller is returned unchanged.
- """
- start_sequence = ansi_style(**kw)
- if start_sequence:
- end_sequence = ANSI_RESET
- if kw.get('readline_hints'):
- end_sequence = readline_wrap(end_sequence)
- return start_sequence + text + end_sequence
- else:
- return text
- def auto_encode(stream, text, *args, **kw):
- """
- Reliably write Unicode strings to the terminal.
- :param stream: The file-like object to write to (a value like
- :data:`sys.stdout` or :data:`sys.stderr`).
- :param text: The text to write to the stream (a string).
- :param args: Refer to :func:`~humanfriendly.text.format()`.
- :param kw: Refer to :func:`~humanfriendly.text.format()`.
- Renders the text using :func:`~humanfriendly.text.format()` and writes it
- to the given stream. If an :exc:`~exceptions.UnicodeEncodeError` is
- encountered in doing so, the text is encoded using :data:`DEFAULT_ENCODING`
- and the write is retried. The reasoning behind this rather blunt approach
- is that it's preferable to get output on the command line in the wrong
- encoding then to have the Python program blow up with a
- :exc:`~exceptions.UnicodeEncodeError` exception.
- """
- text = format(text, *args, **kw)
- try:
- stream.write(text)
- except UnicodeEncodeError:
- stream.write(codecs.encode(text, DEFAULT_ENCODING))
- def clean_terminal_output(text):
- """
- Clean up the terminal output of a command.
- :param text: The raw text with special characters (a Unicode string).
- :returns: A list of Unicode strings (one for each line).
- This function emulates the effect of backspace (0x08), carriage return
- (0x0D) and line feed (0x0A) characters and the ANSI 'erase line' escape
- sequence on interactive terminals. It's intended to clean up command output
- that was originally meant to be rendered on an interactive terminal and
- that has been captured using e.g. the :man:`script` program [#]_ or the
- :mod:`pty` module [#]_.
- .. [#] My coloredlogs_ package supports the ``coloredlogs --to-html``
- command which uses :man:`script` to fool a subprocess into thinking
- that it's connected to an interactive terminal (in order to get it
- to emit ANSI escape sequences).
- .. [#] My capturer_ package uses the :mod:`pty` module to fool the current
- process and subprocesses into thinking they are connected to an
- interactive terminal (in order to get them to emit ANSI escape
- sequences).
- **Some caveats about the use of this function:**
- - Strictly speaking the effect of carriage returns cannot be emulated
- outside of an actual terminal due to the interaction between overlapping
- output, terminal widths and line wrapping. The goal of this function is
- to sanitize noise in terminal output while preserving useful output.
- Think of it as a useful and pragmatic but possibly lossy conversion.
- - The algorithm isn't smart enough to properly handle a pair of ANSI escape
- sequences that open before a carriage return and close after the last
- carriage return in a linefeed delimited string; the resulting string will
- contain only the closing end of the ANSI escape sequence pair. Tracking
- this kind of complexity requires a state machine and proper parsing.
- .. _capturer: https://pypi.org/project/capturer
- .. _coloredlogs: https://pypi.org/project/coloredlogs
- """
- cleaned_lines = []
- current_line = ''
- current_position = 0
- for token in CLEAN_OUTPUT_PATTERN.split(text):
- if token == '\r':
- # Seek back to the start of the current line.
- current_position = 0
- elif token == '\b':
- # Seek back one character in the current line.
- current_position = max(0, current_position - 1)
- else:
- if token == '\n':
- # Capture the current line.
- cleaned_lines.append(current_line)
- if token in ('\n', ANSI_ERASE_LINE):
- # Clear the current line.
- current_line = ''
- current_position = 0
- elif token:
- # Merge regular output into the current line.
- new_position = current_position + len(token)
- prefix = current_line[:current_position]
- suffix = current_line[new_position:]
- current_line = prefix + token + suffix
- current_position = new_position
- # Capture the last line (if any).
- cleaned_lines.append(current_line)
- # Remove any empty trailing lines.
- while cleaned_lines and not cleaned_lines[-1]:
- cleaned_lines.pop(-1)
- return cleaned_lines
- def connected_to_terminal(stream=None):
- """
- Check if a stream is connected to a terminal.
- :param stream: The stream to check (a file-like object,
- defaults to :data:`sys.stdout`).
- :returns: :data:`True` if the stream is connected to a terminal,
- :data:`False` otherwise.
- See also :func:`terminal_supports_colors()`.
- """
- stream = sys.stdout if stream is None else stream
- try:
- return stream.isatty()
- except Exception:
- return False
- @cached
- def enable_ansi_support():
- """
- Try to enable support for ANSI escape sequences (required on Windows).
- :returns: :data:`True` if ANSI is supported, :data:`False` otherwise.
- This functions checks for the following supported configurations, in the
- given order:
- 1. On Windows, if :func:`have_windows_native_ansi_support()` confirms
- native support for ANSI escape sequences :mod:`ctypes` will be used to
- enable this support.
- 2. On Windows, if the environment variable ``$ANSICON`` is set nothing is
- done because it is assumed that support for ANSI escape sequences has
- already been enabled via `ansicon <https://github.com/adoxa/ansicon>`_.
- 3. On Windows, an attempt is made to import and initialize the Python
- package :pypi:`colorama` instead (of course for this to work
- :pypi:`colorama` has to be installed).
- 4. On other platforms this function calls :func:`connected_to_terminal()`
- to determine whether ANSI escape sequences are supported (that is to
- say all platforms that are not Windows are assumed to support ANSI
- escape sequences natively, without weird contortions like above).
- This makes it possible to call :func:`enable_ansi_support()`
- unconditionally without checking the current platform.
- The :func:`~humanfriendly.decorators.cached` decorator is used to ensure
- that this function is only executed once, but its return value remains
- available on later calls.
- """
- if have_windows_native_ansi_support():
- import ctypes
- ctypes.windll.kernel32.SetConsoleMode(ctypes.windll.kernel32.GetStdHandle(-11), 7)
- ctypes.windll.kernel32.SetConsoleMode(ctypes.windll.kernel32.GetStdHandle(-12), 7)
- return True
- elif on_windows():
- if 'ANSICON' in os.environ:
- return True
- try:
- import colorama
- colorama.init()
- return True
- except ImportError:
- return False
- else:
- return connected_to_terminal()
- def find_terminal_size():
- """
- Determine the number of lines and columns visible in the terminal.
- :returns: A tuple of two integers with the line and column count.
- The result of this function is based on the first of the following three
- methods that works:
- 1. First :func:`find_terminal_size_using_ioctl()` is tried,
- 2. then :func:`find_terminal_size_using_stty()` is tried,
- 3. finally :data:`DEFAULT_LINES` and :data:`DEFAULT_COLUMNS` are returned.
- .. note:: The :func:`find_terminal_size()` function performs the steps
- above every time it is called, the result is not cached. This is
- because the size of a virtual terminal can change at any time and
- the result of :func:`find_terminal_size()` should be correct.
- `Pre-emptive snarky comment`_: It's possible to cache the result
- of this function and use :mod:`signal.SIGWINCH <signal>` to
- refresh the cached values!
- Response: As a library I don't consider it the role of the
- :mod:`humanfriendly.terminal` module to install a process wide
- signal handler ...
- .. _Pre-emptive snarky comment: http://blogs.msdn.com/b/oldnewthing/archive/2008/01/30/7315957.aspx
- """
- # The first method. Any of the standard streams may have been redirected
- # somewhere and there's no telling which, so we'll just try them all.
- for stream in sys.stdin, sys.stdout, sys.stderr:
- try:
- result = find_terminal_size_using_ioctl(stream)
- if min(result) >= 1:
- return result
- except Exception:
- pass
- # The second method.
- try:
- result = find_terminal_size_using_stty()
- if min(result) >= 1:
- return result
- except Exception:
- pass
- # Fall back to conservative defaults.
- return DEFAULT_LINES, DEFAULT_COLUMNS
- def find_terminal_size_using_ioctl(stream):
- """
- Find the terminal size using :func:`fcntl.ioctl()`.
- :param stream: A stream connected to the terminal (a file object with a
- ``fileno`` attribute).
- :returns: A tuple of two integers with the line and column count.
- :raises: This function can raise exceptions but I'm not going to document
- them here, you should be using :func:`find_terminal_size()`.
- Based on an `implementation found on StackOverflow <http://stackoverflow.com/a/3010495/788200>`_.
- """
- if not HAVE_IOCTL:
- raise NotImplementedError("It looks like the `fcntl' module is not available!")
- h, w, hp, wp = struct.unpack('HHHH', fcntl.ioctl(stream, termios.TIOCGWINSZ, struct.pack('HHHH', 0, 0, 0, 0)))
- return h, w
- def find_terminal_size_using_stty():
- """
- Find the terminal size using the external command ``stty size``.
- :param stream: A stream connected to the terminal (a file object).
- :returns: A tuple of two integers with the line and column count.
- :raises: This function can raise exceptions but I'm not going to document
- them here, you should be using :func:`find_terminal_size()`.
- """
- stty = subprocess.Popen(['stty', 'size'],
- stdout=subprocess.PIPE,
- stderr=subprocess.PIPE)
- stdout, stderr = stty.communicate()
- tokens = stdout.split()
- if len(tokens) != 2:
- raise Exception("Invalid output from `stty size'!")
- return tuple(map(int, tokens))
- def get_pager_command(text=None):
- """
- Get the command to show a text on the terminal using a pager.
- :param text: The text to print to the terminal (a string).
- :returns: A list of strings with the pager command and arguments.
- The use of a pager helps to avoid the wall of text effect where the user
- has to scroll up to see where the output began (not very user friendly).
- If the given text contains ANSI escape sequences the command ``less
- --RAW-CONTROL-CHARS`` is used, otherwise the environment variable
- ``$PAGER`` is used (if ``$PAGER`` isn't set :man:`less` is used).
- When the selected pager is :man:`less`, the following options are used to
- make the experience more user friendly:
- - ``--quit-if-one-screen`` causes :man:`less` to automatically exit if the
- entire text can be displayed on the first screen. This makes the use of a
- pager transparent for smaller texts (because the operator doesn't have to
- quit the pager).
- - ``--no-init`` prevents :man:`less` from clearing the screen when it
- exits. This ensures that the operator gets a chance to review the text
- (for example a usage message) after quitting the pager, while composing
- the next command.
- """
- # Compose the pager command.
- if text and ANSI_CSI in text:
- command_line = ['less', '--RAW-CONTROL-CHARS']
- else:
- command_line = [os.environ.get('PAGER', 'less')]
- # Pass some additional options to `less' (to make it more
- # user friendly) without breaking support for other pagers.
- if os.path.basename(command_line[0]) == 'less':
- command_line.append('--no-init')
- command_line.append('--quit-if-one-screen')
- return command_line
- @cached
- def have_windows_native_ansi_support():
- """
- Check if we're running on a Windows 10 release with native support for ANSI escape sequences.
- :returns: :data:`True` if so, :data:`False` otherwise.
- The :func:`~humanfriendly.decorators.cached` decorator is used as a minor
- performance optimization. Semantically this should have zero impact because
- the answer doesn't change in the lifetime of a computer process.
- """
- if on_windows():
- try:
- # I can't be 100% sure this will never break and I'm not in a
- # position to test it thoroughly either, so I decided that paying
- # the price of one additional try / except statement is worth the
- # additional peace of mind :-).
- components = tuple(int(c) for c in platform.version().split('.'))
- return components >= (10, 0, 14393)
- except Exception:
- pass
- return False
- def message(text, *args, **kw):
- """
- Print a formatted message to the standard error stream.
- For details about argument handling please refer to
- :func:`~humanfriendly.text.format()`.
- Renders the message using :func:`~humanfriendly.text.format()` and writes
- the resulting string (followed by a newline) to :data:`sys.stderr` using
- :func:`auto_encode()`.
- """
- auto_encode(sys.stderr, coerce_string(text) + '\n', *args, **kw)
- def output(text, *args, **kw):
- """
- Print a formatted message to the standard output stream.
- For details about argument handling please refer to
- :func:`~humanfriendly.text.format()`.
- Renders the message using :func:`~humanfriendly.text.format()` and writes
- the resulting string (followed by a newline) to :data:`sys.stdout` using
- :func:`auto_encode()`.
- """
- auto_encode(sys.stdout, coerce_string(text) + '\n', *args, **kw)
- def readline_strip(expr):
- """
- Remove `readline hints`_ from a string.
- :param text: The text to strip (a string).
- :returns: The stripped text.
- """
- return expr.replace('\001', '').replace('\002', '')
- def readline_wrap(expr):
- """
- Wrap an ANSI escape sequence in `readline hints`_.
- :param text: The text with the escape sequence to wrap (a string).
- :returns: The wrapped text.
- .. _readline hints: http://superuser.com/a/301355
- """
- return '\001' + expr + '\002'
- def show_pager(formatted_text, encoding=DEFAULT_ENCODING):
- """
- Print a large text to the terminal using a pager.
- :param formatted_text: The text to print to the terminal (a string).
- :param encoding: The name of the text encoding used to encode the formatted
- text if the formatted text is a Unicode string (a string,
- defaults to :data:`DEFAULT_ENCODING`).
- When :func:`connected_to_terminal()` returns :data:`True` a pager is used
- to show the text on the terminal, otherwise the text is printed directly
- without invoking a pager.
- The use of a pager helps to avoid the wall of text effect where the user
- has to scroll up to see where the output began (not very user friendly).
- Refer to :func:`get_pager_command()` for details about the command line
- that's used to invoke the pager.
- """
- if connected_to_terminal():
- # Make sure the selected pager command is available.
- command_line = get_pager_command(formatted_text)
- if which(command_line[0]):
- pager = subprocess.Popen(command_line, stdin=subprocess.PIPE)
- if is_unicode(formatted_text):
- formatted_text = formatted_text.encode(encoding)
- pager.communicate(input=formatted_text)
- return
- output(formatted_text)
- def terminal_supports_colors(stream=None):
- """
- Check if a stream is connected to a terminal that supports ANSI escape sequences.
- :param stream: The stream to check (a file-like object,
- defaults to :data:`sys.stdout`).
- :returns: :data:`True` if the terminal supports ANSI escape sequences,
- :data:`False` otherwise.
- This function was originally inspired by the implementation of
- `django.core.management.color.supports_color()
- <https://github.com/django/django/blob/master/django/core/management/color.py>`_
- but has since evolved significantly.
- """
- if on_windows():
- # On Windows support for ANSI escape sequences is not a given.
- have_ansicon = 'ANSICON' in os.environ
- have_colorama = 'colorama' in sys.modules
- have_native_support = have_windows_native_ansi_support()
- if not (have_ansicon or have_colorama or have_native_support):
- return False
- return connected_to_terminal(stream)
- def usage(usage_text):
- """
- Print a human friendly usage message to the terminal.
- :param text: The usage message to print (a string).
- This function does two things:
- 1. If :data:`sys.stdout` is connected to a terminal (see
- :func:`connected_to_terminal()`) then the usage message is formatted
- using :func:`.format_usage()`.
- 2. The usage message is shown using a pager (see :func:`show_pager()`).
- """
- if terminal_supports_colors(sys.stdout):
- usage_text = format_usage(usage_text)
- show_pager(usage_text)
- def warning(text, *args, **kw):
- """
- Show a warning message on the terminal.
- For details about argument handling please refer to
- :func:`~humanfriendly.text.format()`.
- Renders the message using :func:`~humanfriendly.text.format()` and writes
- the resulting string (followed by a newline) to :data:`sys.stderr` using
- :func:`auto_encode()`.
- If :data:`sys.stderr` is connected to a terminal that supports colors,
- :func:`ansi_wrap()` is used to color the message in a red font (to make
- the warning stand out from surrounding text).
- """
- text = coerce_string(text)
- if terminal_supports_colors(sys.stderr):
- text = ansi_wrap(text, color='red')
- auto_encode(sys.stderr, text + '\n', *args, **kw)
- # Define aliases for backwards compatibility.
- define_aliases(
- module_name=__name__,
- # In humanfriendly 1.31 the find_meta_variables() and format_usage()
- # functions were extracted to the new module humanfriendly.usage.
- find_meta_variables='humanfriendly.usage.find_meta_variables',
- format_usage='humanfriendly.usage.format_usage',
- # In humanfriendly 8.0 the html_to_ansi() function and HTMLConverter
- # class were extracted to the new module humanfriendly.terminal.html.
- html_to_ansi='humanfriendly.terminal.html.html_to_ansi',
- HTMLConverter='humanfriendly.terminal.html.HTMLConverter',
- )
|