#!/usr/bin/env python
# encoding: utf-8
# Copyright (C) 2015 Chintalagiri Shashank
#
# This file is part of tendril.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
The Terminal Utils Module (:mod:`tendril.utils.terminal`)
=========================================================
This module provides utils for rendering basic UI elements on the terminal.
:class:`TendrilProgressBar` can be used to produce animated progress bars
on the terminal. This class (and the code related to it) is essentially a
copy of pip's progressbar implementation in pip.utils.ui.
"""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import sys
import os
import shlex
import struct
import platform
import subprocess
import six
from time import time
from progress.bar import Bar
from progress.bar import IncrementalBar
try:
import colorama
# Lots of different errors can come from this, including SystemError and
# ImportError.
except Exception:
colorama = None
WINDOWS = (sys.platform.startswith("win") or
(sys.platform == 'cli' and os.name == 'nt'))
[docs]def get_terminal_width():
return get_terminal_size()[0]
[docs]def render_hline():
width = get_terminal_width()
hline = '-' * width
print(hline)
[docs]def get_terminal_size():
"""
getTerminalSize()
- get width and height of console
- works on linux,os x,windows,cygwin(windows)
Taken from https://gist.github.com/jtriley/1108174
"""
current_os = platform.system()
tuple_xy = None
if current_os == 'Windows':
tuple_xy = _get_terminal_size_windows()
if tuple_xy is None:
tuple_xy = _get_terminal_size_tput()
# needed for window's python in cygwin's xterm!
if current_os in ['Linux', 'Darwin'] or current_os.startswith('CYGWIN'):
tuple_xy = _get_terminal_size_linux()
if tuple_xy is None:
tuple_xy = (80, 25) # default value
return tuple_xy
[docs]def _get_terminal_size_windows():
try:
from ctypes import windll, create_string_buffer
# stdin handle is -10
# stdout handle is -11
# stderr handle is -12
h = windll.kernel32.GetStdHandle(-12)
csbi = create_string_buffer(22)
res = windll.kernel32.GetConsoleScreenBufferInfo(h, csbi)
if res:
(bufx, bufy, curx, cury, wattr,
left, top, right, bottom,
maxx, maxy) = struct.unpack("hhhhHhhhhhh", csbi.raw)
sizex = right - left + 1
sizey = bottom - top + 1
return sizex, sizey
except:
pass
[docs]def _get_terminal_size_tput():
# get terminal width
try:
cols = int(subprocess.check_call(shlex.split('tput cols')))
rows = int(subprocess.check_call(shlex.split('tput lines')))
return cols, rows
except:
pass
[docs]def _get_terminal_size_linux():
def ioctl_GWINSZ(fd):
try:
import fcntl
import termios
cr = struct.unpack('hh',
fcntl.ioctl(fd, termios.TIOCGWINSZ, '1234'))
return cr
except:
pass
cr = ioctl_GWINSZ(0) or ioctl_GWINSZ(1) or ioctl_GWINSZ(2)
if not cr:
try:
fd = os.open(os.ctermid(), os.O_RDONLY)
cr = ioctl_GWINSZ(fd)
os.close(fd)
except:
pass
if not cr:
try:
cr = (os.environ['LINES'], os.environ['COLUMNS'])
except:
return None
return int(cr[1]), int(cr[0])
[docs]def _select_progress_class(preferred, fallback):
encoding = getattr(preferred.file, "encoding", None)
# If we don't know what encoding this file is in, then we'll just assume
# that it doesn't support unicode and use the ASCII bar.
if not encoding:
return fallback
# Collect all of the possible characters we want to use with the preferred
# bar.
characters = [
getattr(preferred, "empty_fill", six.text_type()),
getattr(preferred, "fill", six.text_type()),
]
characters += list(getattr(preferred, "phases", []))
# Try to decode the characters we're using for the bar using the encoding
# of the given file, if this works then we'll assume that we can use the
# fancier bar and if not we'll fall back to the plaintext bar.
try:
six.text_type().join(characters).encode(encoding)
except UnicodeEncodeError:
return fallback
else:
return preferred
_BaseBar = _select_progress_class(IncrementalBar, Bar)
[docs]class WindowsMixin(object):
def __init__(self, *args, **kwargs):
# The Windows terminal does not support the hide/show cursor ANSI
# codes even with colorama. So we'll ensure that hide_cursor is False
# on Windows.
# This call neds to go before the super() call, so that hide_cursor
# is set in time. The base progress bar class writes the "hide cursor"
# code to the terminal in its init, so if we don't set this soon
# enough, we get a "hide" with no corresponding "show"...
if WINDOWS and self.hide_cursor:
self.hide_cursor = False
super(WindowsMixin, self).__init__(*args, **kwargs)
# Check if we are running on Windows and we have the colorama module,
# if we do then wrap our file with it.
if WINDOWS and colorama:
self.file = colorama.AnsiToWin32(self.file)
# The progress code expects to be able to call self.file.isatty()
# but the colorama.AnsiToWin32() object doesn't have that, so
# we'll add it.
self.file.isatty = lambda: self.file.wrapped.isatty()
# The progress code expects to be able to call self.file.flush()
# but the colorama.AnsiToWin32() object doesn't have that, so
# we'll add it.
self.file.flush = lambda: self.file.wrapped.flush()
[docs]class TendrilProgressBar(WindowsMixin, _BaseBar):
"""
This class can be used from other modules to provide a consistent
feel to progress bars across tendril. It adds a ``note`` keyword
argument to the ``next()`` function, and renders the note after
the suffix of the progress bar.
.. rubric :: Usage
>>> from tendril.utils.terminal import TendrilProgressBar
>>> pb = TendrilProgressBar(max=100)
>>> for i in range(100):
... pb.next(note=i)
"""
file = sys.stdout
message = "%(percent)3d%%"
suffix = "ETA %(eta_td)s"
def __init__(self, *args, **kwargs):
super(TendrilProgressBar, self).__init__(*args, **kwargs)
self._note = None
self._term_width = get_terminal_width()
@property
def term_width(self):
return self._term_width
[docs] def next(self, n=1, note=None):
if n > 0:
now = time()
dt = (now - self._ts) / n
self._dt.append(dt)
self._ts = now
self.index += n
self._note = str(note)
self.update()
[docs] def writeln(self, line):
if self.file.isatty():
self.clearln()
if self._note is not None:
line = ' '.join([line, self._note])
if len(line) > self.term_width:
line = line[:self.term_width]
print(line, end='', file=self.file)
self.file.flush()
[docs]class DummyProgressBar(WindowsMixin, _BaseBar):
"""
This class can be used from other modules to provide a dummy
progress bar like object, with interfaces consistent with
:class:`TendrilProgressBar`.
"""
file = sys.stdout
message = "%(percent)3d%%"
def __init__(self, *args, **kwargs):
super(DummyProgressBar, self).__init__(*args, **kwargs)
self._note = None
self._term_width = get_terminal_width()
@property
def term_width(self):
return self._term_width
[docs] def next(self, n=1, note=None):
if n > 0:
now = time()
dt = (now - self._ts) / n
self._dt.append(dt)
self._ts = now
self.index += n
self._note = str(note)
self.update()
[docs] def update(self):
message = self.message % self
line = message
self.writeln(line)
[docs] def writeln(self, line):
if self.file.isatty():
if self._note is not None:
line = ' '.join([line, self._note])
if len(line) > self.term_width:
line = line[:self.term_width]
print(line, file=self.file)
self.file.flush()
[docs] def finish(self):
if self.hide_cursor:
print('\x1b[?25h', end='', file=self.file)