Initialisation du repository de Beta
This commit is contained in:
commit
14985f6dbb
9469 changed files with 1903273 additions and 0 deletions
|
|
@ -0,0 +1,776 @@
|
|||
# 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',
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue