Blame dnf/cli/term.py

Packit 6f3914
# Copyright (C) 2013-2014  Red Hat, Inc.
Packit 6f3914
# Terminal routines.
Packit 6f3914
#
Packit 6f3914
# This copyrighted material is made available to anyone wishing to use,
Packit 6f3914
# modify, copy, or redistribute it subject to the terms and conditions of
Packit 6f3914
# the GNU General Public License v.2, or (at your option) any later version.
Packit 6f3914
# This program is distributed in the hope that it will be useful, but WITHOUT
Packit 6f3914
# ANY WARRANTY expressed or implied, including the implied warranties of
Packit 6f3914
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
Packit 6f3914
# Public License for more details.  You should have received a copy of the
Packit 6f3914
# GNU General Public License along with this program; if not, write to the
Packit 6f3914
# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
Packit 6f3914
# 02110-1301, USA.  Any Red Hat trademarks that are incorporated in the
Packit 6f3914
# source code or documentation are not subject to the GNU General Public
Packit 6f3914
# License and may only be used or replicated with the express permission of
Packit 6f3914
# Red Hat, Inc.
Packit 6f3914
#
Packit 6f3914
Packit 6f3914
from __future__ import absolute_import
Packit 6f3914
from __future__ import unicode_literals
Packit 6f3914
import curses
Packit 6f3914
import dnf.pycomp
Packit 6f3914
import fcntl
Packit 6f3914
import re
Packit 6f3914
import struct
Packit 6f3914
import sys
Packit 6f3914
import termios
Packit 6f3914
Packit 6f3914
Packit 6f3914
def _real_term_width(fd=1):
Packit 6f3914
    """ Get the real terminal width """
Packit 6f3914
    try:
Packit 6f3914
        buf = 'abcdefgh'
Packit 6f3914
        buf = fcntl.ioctl(fd, termios.TIOCGWINSZ, buf)
Packit 6f3914
        ret = struct.unpack(b'hhhh', buf)[1]
Packit 6f3914
        return ret
Packit 6f3914
    except IOError:
Packit 6f3914
        return None
Packit 6f3914
Packit 6f3914
Packit 6f3914
def _term_width(fd=1):
Packit 6f3914
    """ Compute terminal width falling to default 80 in case of trouble"""
Packit 6f3914
    tw = _real_term_width(fd=1)
Packit 6f3914
    if not tw:
Packit 6f3914
        return 80
Packit 6f3914
    elif tw < 20:
Packit 6f3914
        return 20
Packit 6f3914
    else:
Packit 6f3914
        return tw
Packit 6f3914
Packit 6f3914
Packit 6f3914
class Term(object):
Packit 6f3914
    """A class to provide some terminal "UI" helpers based on curses."""
Packit 6f3914
Packit 6f3914
    # From initial search for "terminfo and python" got:
Packit 6f3914
    # http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/475116
Packit 6f3914
    # ...it's probably not copyrightable, but if so ASPN says:
Packit 6f3914
    #
Packit 6f3914
    #  Except where otherwise noted, recipes in the Python Cookbook are
Packit 6f3914
    # published under the Python license.
Packit 6f3914
Packit 6f3914
    __enabled = True
Packit 6f3914
Packit 6f3914
    real_columns = property(lambda self: _real_term_width())
Packit 6f3914
    columns = property(lambda self: _term_width())
Packit 6f3914
Packit 6f3914
    __cap_names = {
Packit 6f3914
        'underline' : 'smul',
Packit 6f3914
        'reverse' : 'rev',
Packit 6f3914
        'normal' : 'sgr0',
Packit 6f3914
        }
Packit 6f3914
Packit 6f3914
    __colors = {
Packit 6f3914
        'black' : 0,
Packit 6f3914
        'blue' : 1,
Packit 6f3914
        'green' : 2,
Packit 6f3914
        'cyan' : 3,
Packit 6f3914
        'red' : 4,
Packit 6f3914
        'magenta' : 5,
Packit 6f3914
        'yellow' : 6,
Packit 6f3914
        'white' : 7
Packit 6f3914
        }
Packit 6f3914
    __ansi_colors = {
Packit 6f3914
        'black' : 0,
Packit 6f3914
        'red' : 1,
Packit 6f3914
        'green' : 2,
Packit 6f3914
        'yellow' : 3,
Packit 6f3914
        'blue' : 4,
Packit 6f3914
        'magenta' : 5,
Packit 6f3914
        'cyan' : 6,
Packit 6f3914
        'white' : 7
Packit 6f3914
        }
Packit 6f3914
    __ansi_forced_MODE = {
Packit 6f3914
        'bold' : '\x1b[1m',
Packit 6f3914
        'blink' : '\x1b[5m',
Packit 6f3914
        'dim' : '',
Packit 6f3914
        'reverse' : '\x1b[7m',
Packit 6f3914
        'underline' : '\x1b[4m',
Packit 6f3914
        'normal' : '\x1b(B\x1b[m'
Packit 6f3914
        }
Packit 6f3914
    __ansi_forced_FG_COLOR = {
Packit 6f3914
        'black' : '\x1b[30m',
Packit 6f3914
        'red' : '\x1b[31m',
Packit 6f3914
        'green' : '\x1b[32m',
Packit 6f3914
        'yellow' : '\x1b[33m',
Packit 6f3914
        'blue' : '\x1b[34m',
Packit 6f3914
        'magenta' : '\x1b[35m',
Packit 6f3914
        'cyan' : '\x1b[36m',
Packit 6f3914
        'white' : '\x1b[37m'
Packit 6f3914
        }
Packit 6f3914
    __ansi_forced_BG_COLOR = {
Packit 6f3914
        'black' : '\x1b[40m',
Packit 6f3914
        'red' : '\x1b[41m',
Packit 6f3914
        'green' : '\x1b[42m',
Packit 6f3914
        'yellow' : '\x1b[43m',
Packit 6f3914
        'blue' : '\x1b[44m',
Packit 6f3914
        'magenta' : '\x1b[45m',
Packit 6f3914
        'cyan' : '\x1b[46m',
Packit 6f3914
        'white' : '\x1b[47m'
Packit 6f3914
        }
Packit 6f3914
Packit 6f3914
    def __forced_init(self):
Packit 6f3914
        self.MODE = self.__ansi_forced_MODE
Packit 6f3914
        self.FG_COLOR = self.__ansi_forced_FG_COLOR
Packit 6f3914
        self.BG_COLOR = self.__ansi_forced_BG_COLOR
Packit 6f3914
Packit 6f3914
    def reinit(self, term_stream=None, color='auto'):
Packit 6f3914
        """Reinitializes the :class:`Term`.
Packit 6f3914
Packit 6f3914
        :param term_stream:  the terminal stream that the
Packit 6f3914
           :class:`Term` should be initialized to use.  If
Packit 6f3914
           *term_stream* is not given, :attr:`sys.stdout` is used.
Packit 6f3914
        :param color: when to colorize output.  Valid values are
Packit 6f3914
           'always', 'auto', and 'never'.  'always' will use ANSI codes
Packit 6f3914
           to always colorize output, 'auto' will decide whether do
Packit 6f3914
           colorize depending on the terminal, and 'never' will never
Packit 6f3914
           colorize.
Packit 6f3914
        """
Packit 6f3914
        self.__enabled = True
Packit 6f3914
        self.lines = 24
Packit 6f3914
Packit 6f3914
        if color == 'always':
Packit 6f3914
            self.__forced_init()
Packit 6f3914
            return
Packit 6f3914
Packit 6f3914
        # Output modes:
Packit 6f3914
        self.MODE = {
Packit 6f3914
            'bold' : '',
Packit 6f3914
            'blink' : '',
Packit 6f3914
            'dim' : '',
Packit 6f3914
            'reverse' : '',
Packit 6f3914
            'underline' : '',
Packit 6f3914
            'normal' : ''
Packit 6f3914
            }
Packit 6f3914
Packit 6f3914
        # Colours
Packit 6f3914
        self.FG_COLOR = {
Packit 6f3914
            'black' : '',
Packit 6f3914
            'blue' : '',
Packit 6f3914
            'green' : '',
Packit 6f3914
            'cyan' : '',
Packit 6f3914
            'red' : '',
Packit 6f3914
            'magenta' : '',
Packit 6f3914
            'yellow' : '',
Packit 6f3914
            'white' : ''
Packit 6f3914
            }
Packit 6f3914
Packit 6f3914
        self.BG_COLOR = {
Packit 6f3914
            'black' : '',
Packit 6f3914
            'blue' : '',
Packit 6f3914
            'green' : '',
Packit 6f3914
            'cyan' : '',
Packit 6f3914
            'red' : '',
Packit 6f3914
            'magenta' : '',
Packit 6f3914
            'yellow' : '',
Packit 6f3914
            'white' : ''
Packit 6f3914
            }
Packit 6f3914
Packit 6f3914
        if color == 'never':
Packit 6f3914
            self.__enabled = False
Packit 6f3914
            return
Packit 6f3914
        assert color == 'auto'
Packit 6f3914
Packit 6f3914
        # If the stream isn't a tty, then assume it has no capabilities.
Packit 6f3914
        if not term_stream:
Packit 6f3914
            term_stream = sys.stdout
Packit 6f3914
        if not term_stream.isatty():
Packit 6f3914
            self.__enabled = False
Packit 6f3914
            return
Packit 6f3914
Packit 6f3914
        # Check the terminal type.  If we fail, then assume that the
Packit 6f3914
        # terminal has no capabilities.
Packit 6f3914
        try:
Packit 6f3914
            curses.setupterm(fd=term_stream.fileno())
Packit 6f3914
        except Exception:
Packit 6f3914
            self.__enabled = False
Packit 6f3914
            return
Packit 6f3914
        self._ctigetstr = curses.tigetstr
Packit 6f3914
Packit 6f3914
        self.lines = curses.tigetnum('lines')
Packit 6f3914
Packit 6f3914
        # Look up string capabilities.
Packit 6f3914
        for cap_name in self.MODE:
Packit 6f3914
            mode = cap_name
Packit 6f3914
            if cap_name in self.__cap_names:
Packit 6f3914
                cap_name = self.__cap_names[cap_name]
Packit 6f3914
            self.MODE[mode] = self._tigetstr(cap_name)
Packit 6f3914
Packit 6f3914
        # Colors
Packit 6f3914
        set_fg = self._tigetstr('setf').encode('utf-8')
Packit 6f3914
        if set_fg:
Packit 6f3914
            for (color, val) in self.__colors.items():
Packit 6f3914
                self.FG_COLOR[color] = curses.tparm(set_fg, val).decode() or ''
Packit 6f3914
        set_fg_ansi = self._tigetstr('setaf').encode('utf-8')
Packit 6f3914
        if set_fg_ansi:
Packit 6f3914
            for (color, val) in self.__ansi_colors.items():
Packit 6f3914
                fg_color = curses.tparm(set_fg_ansi, val).decode() or ''
Packit 6f3914
                self.FG_COLOR[color] = fg_color
Packit 6f3914
        set_bg = self._tigetstr('setb').encode('utf-8')
Packit 6f3914
        if set_bg:
Packit 6f3914
            for (color, val) in self.__colors.items():
Packit 6f3914
                self.BG_COLOR[color] = curses.tparm(set_bg, val).decode() or ''
Packit 6f3914
        set_bg_ansi = self._tigetstr('setab').encode('utf-8')
Packit 6f3914
        if set_bg_ansi:
Packit 6f3914
            for (color, val) in self.__ansi_colors.items():
Packit 6f3914
                bg_color = curses.tparm(set_bg_ansi, val).decode() or ''
Packit 6f3914
                self.BG_COLOR[color] = bg_color
Packit 6f3914
Packit 6f3914
    def __init__(self, term_stream=None, color='auto'):
Packit 6f3914
        self.reinit(term_stream, color)
Packit 6f3914
Packit 6f3914
    def _tigetstr(self, cap_name):
Packit 6f3914
        # String capabilities can include "delays" of the form "$<2>".
Packit 6f3914
        # For any modern terminal, we should be able to just ignore
Packit 6f3914
        # these, so strip them out.
Packit 6f3914
        cap = self._ctigetstr(cap_name) or ''
Packit 6f3914
        if dnf.pycomp.is_py3bytes(cap):
Packit 6f3914
            cap = cap.decode()
Packit 6f3914
        return re.sub(r'\$<\d+>[/*]?', '', cap)
Packit 6f3914
Packit 6f3914
    def color(self, color, s):
Packit 6f3914
        """Colorize string with color"""
Packit 6f3914
        return (self.MODE[color] + str(s) + self.MODE['normal'])
Packit 6f3914
Packit 6f3914
    def bold(self, s):
Packit 6f3914
        """Make string bold."""
Packit 6f3914
        return self.color('bold', s)
Packit 6f3914
Packit 6f3914
    def sub(self, haystack, beg, end, needles, escape=None, ignore_case=False):
Packit 6f3914
        """Search the string *haystack* for all occurrences of any
Packit 6f3914
        string in the list *needles*.  Prefix each occurrence with
Packit 6f3914
        *beg*, and postfix each occurrence with *end*, then return the
Packit 6f3914
        modified string.  For example::
Packit 6f3914
Packit 6f3914
           >>> yt = Term()
Packit 6f3914
           >>> yt.sub('spam and eggs', 'x', 'z', ['and'])
Packit 6f3914
           'spam xandz eggs'
Packit 6f3914
Packit 6f3914
        This is particularly useful for emphasizing certain words
Packit 6f3914
        in output: for example, calling :func:`sub` with *beg* =
Packit 6f3914
        MODE['bold'] and *end* = MODE['normal'] will return a string
Packit 6f3914
        that when printed to the terminal will appear to be *haystack*
Packit 6f3914
        with each occurrence of the strings in *needles* in bold
Packit 6f3914
        face.  Note, however, that the :func:`sub_mode`,
Packit 6f3914
        :func:`sub_bold`, :func:`sub_fg`, and :func:`sub_bg` methods
Packit 6f3914
        provide convenient ways to access this same emphasizing functionality.
Packit 6f3914
Packit 6f3914
        :param haystack: the string to be modified
Packit 6f3914
        :param beg: the string to be prefixed onto matches
Packit 6f3914
        :param end: the string to be postfixed onto matches
Packit 6f3914
        :param needles: a list of strings to add the prefixes and
Packit 6f3914
           postfixes to
Packit 6f3914
        :param escape: a function that accepts a string and returns
Packit 6f3914
           the same string with problematic characters escaped.  By
Packit 6f3914
           default, :func:`re.escape` is used.
Packit 6f3914
        :param ignore_case: whether case should be ignored when
Packit 6f3914
           searching for matches
Packit 6f3914
        :return: *haystack* with *beg* prefixing, and *end*
Packit 6f3914
          postfixing, occurrences of the strings in *needles*
Packit 6f3914
        """
Packit 6f3914
        if not self.__enabled:
Packit 6f3914
            return haystack
Packit 6f3914
Packit 6f3914
        if not escape:
Packit 6f3914
            escape = re.escape
Packit 6f3914
Packit 6f3914
        render = lambda match: beg + match.group() + end
Packit 6f3914
        for needle in needles:
Packit 6f3914
            pat = escape(needle)
Packit 6f3914
            if ignore_case:
Packit 6f3914
                pat = re.template(pat, re.I)
Packit 6f3914
            haystack = re.sub(pat, render, haystack)
Packit 6f3914
        return haystack
Packit 6f3914
    def sub_norm(self, haystack, beg, needles, **kwds):
Packit 6f3914
        """Search the string *haystack* for all occurrences of any
Packit 6f3914
        string in the list *needles*.  Prefix each occurrence with
Packit 6f3914
        *beg*, and postfix each occurrence with self.MODE['normal'],
Packit 6f3914
        then return the modified string.  If *beg* is an ANSI escape
Packit 6f3914
        code, such as given by self.MODE['bold'], this method will
Packit 6f3914
        return *haystack* with the formatting given by the code only
Packit 6f3914
        applied to the strings in *needles*.
Packit 6f3914
Packit 6f3914
        :param haystack: the string to be modified
Packit 6f3914
        :param beg: the string to be prefixed onto matches
Packit 6f3914
        :param end: the string to be postfixed onto matches
Packit 6f3914
        :param needles: a list of strings to add the prefixes and
Packit 6f3914
           postfixes to
Packit 6f3914
        :return: *haystack* with *beg* prefixing, and self.MODE['normal']
Packit 6f3914
          postfixing, occurrences of the strings in *needles*
Packit 6f3914
        """
Packit 6f3914
        return self.sub(haystack, beg, self.MODE['normal'], needles, **kwds)
Packit 6f3914
Packit 6f3914
    def sub_mode(self, haystack, mode, needles, **kwds):
Packit 6f3914
        """Search the string *haystack* for all occurrences of any
Packit 6f3914
        string in the list *needles*.  Prefix each occurrence with
Packit 6f3914
        self.MODE[*mode*], and postfix each occurrence with
Packit 6f3914
        self.MODE['normal'], then return the modified string.  This
Packit 6f3914
        will return a string that when printed to the terminal will
Packit 6f3914
        appear to be *haystack* with each occurrence of the strings in
Packit 6f3914
        *needles* in the given *mode*.
Packit 6f3914
Packit 6f3914
        :param haystack: the string to be modified
Packit 6f3914
        :param mode: the mode to set the matches to be in.  Valid
Packit 6f3914
           values are given by self.MODE.keys().
Packit 6f3914
        :param needles: a list of strings to add the prefixes and
Packit 6f3914
           postfixes to
Packit 6f3914
        :return: *haystack* with self.MODE[*mode*] prefixing, and
Packit 6f3914
          self.MODE['normal'] postfixing, occurrences of the strings
Packit 6f3914
          in *needles*
Packit 6f3914
        """
Packit 6f3914
        return self.sub_norm(haystack, self.MODE[mode], needles, **kwds)
Packit 6f3914
Packit 6f3914
    def sub_bold(self, haystack, needles, **kwds):
Packit 6f3914
        """Search the string *haystack* for all occurrences of any
Packit 6f3914
        string in the list *needles*.  Prefix each occurrence with
Packit 6f3914
        self.MODE['bold'], and postfix each occurrence with
Packit 6f3914
        self.MODE['normal'], then return the modified string.  This
Packit 6f3914
        will return a string that when printed to the terminal will
Packit 6f3914
        appear to be *haystack* with each occurrence of the strings in
Packit 6f3914
        *needles* in bold face.
Packit 6f3914
Packit 6f3914
        :param haystack: the string to be modified
Packit 6f3914
        :param needles: a list of strings to add the prefixes and
Packit 6f3914
           postfixes to
Packit 6f3914
        :return: *haystack* with self.MODE['bold'] prefixing, and
Packit 6f3914
          self.MODE['normal'] postfixing, occurrences of the strings
Packit 6f3914
          in *needles*
Packit 6f3914
        """
Packit 6f3914
        return self.sub_mode(haystack, 'bold', needles, **kwds)
Packit 6f3914
Packit 6f3914
    def sub_fg(self, haystack, color, needles, **kwds):
Packit 6f3914
        """Search the string *haystack* for all occurrences of any
Packit 6f3914
        string in the list *needles*.  Prefix each occurrence with
Packit 6f3914
        self.FG_COLOR[*color*], and postfix each occurrence with
Packit 6f3914
        self.MODE['normal'], then return the modified string.  This
Packit 6f3914
        will return a string that when printed to the terminal will
Packit 6f3914
        appear to be *haystack* with each occurrence of the strings in
Packit 6f3914
        *needles* in the given color.
Packit 6f3914
Packit 6f3914
        :param haystack: the string to be modified
Packit 6f3914
        :param color: the color to set the matches to be in.  Valid
Packit 6f3914
           values are given by self.FG_COLOR.keys().
Packit 6f3914
        :param needles: a list of strings to add the prefixes and
Packit 6f3914
           postfixes to
Packit 6f3914
        :return: *haystack* with self.FG_COLOR[*color*] prefixing, and
Packit 6f3914
          self.MODE['normal'] postfixing, occurrences of the strings
Packit 6f3914
          in *needles*
Packit 6f3914
        """
Packit 6f3914
        return self.sub_norm(haystack, self.FG_COLOR[color], needles, **kwds)
Packit 6f3914
Packit 6f3914
    def sub_bg(self, haystack, color, needles, **kwds):
Packit 6f3914
        """Search the string *haystack* for all occurrences of any
Packit 6f3914
        string in the list *needles*.  Prefix each occurrence with
Packit 6f3914
        self.BG_COLOR[*color*], and postfix each occurrence with
Packit 6f3914
        self.MODE['normal'], then return the modified string.  This
Packit 6f3914
        will return a string that when printed to the terminal will
Packit 6f3914
        appear to be *haystack* with each occurrence of the strings in
Packit 6f3914
        *needles* highlighted in the given background color.
Packit 6f3914
Packit 6f3914
        :param haystack: the string to be modified
Packit 6f3914
        :param color: the background color to set the matches to be in.  Valid
Packit 6f3914
           values are given by self.BG_COLOR.keys().
Packit 6f3914
        :param needles: a list of strings to add the prefixes and
Packit 6f3914
           postfixes to
Packit 6f3914
        :return: *haystack* with self.BG_COLOR[*color*] prefixing, and
Packit 6f3914
          self.MODE['normal'] postfixing, occurrences of the strings
Packit 6f3914
          in *needles*
Packit 6f3914
        """
Packit 6f3914
        return self.sub_norm(haystack, self.BG_COLOR[color], needles, **kwds)