From a907a747af78e1ea4de791ba7345e35ebcd3b5fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Elliott=20B=C3=A9cue?= Date: Wed, 30 Apr 2014 21:33:16 +0200 Subject: [PATCH] =?UTF-8?q?[gestion/affichage]=20Une=20tentative=20de=20cr?= =?UTF-8?q?=C3=A9er=20un=20nouvel=20affich=5Ftools.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fonctionnalités : * Gère les animations, "kikoo" ou non ; * Propose une fonction d'affichage stylisé (couleur, gras) plus complète ; * Propose une fonction récupérant la taille du terminal adaptée à pas mal d'environnements ; * Quelques bricoles pour limiter la taille d'un texte et formatter les pourcentages, tout en choisissant une couleur en fonction de ceux-ci. * Propose une fonction prettyDoin qui permet de faire de l'affichage service-like pour des opérations en cours. Je le laisse dans gestion/ pour que les gens voient qu'il concurrence affich_tools. --- gestion/affichage.py | 261 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 261 insertions(+) create mode 100755 gestion/affichage.py diff --git a/gestion/affichage.py b/gestion/affichage.py new file mode 100755 index 00000000..57c8dfde --- /dev/null +++ b/gestion/affichage.py @@ -0,0 +1,261 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import sys +import os +import fcntl +import termios +import struct +import functools + +def static_var(name, val): + """Decorator setting static variable + to a function. + + """ + def decorate(fun): + functools.wraps(fun) + setattr(fun, name, val) + return fun + return decorate + +def getTerminalSize(): + """Dummy function to get term dimensions. + Thanks to http://stackoverflow.com/questions/566746/how-to-get-console-window-width-in-python + + Could be done using only env variables or thing like stty size, but would be less + portable. + + """ + env = os.environ + def ioctl_GWINSZ(fd): + try: + cr = struct.unpack('hh', fcntl.ioctl(fd, termios.TIOCGWINSZ, '1234')) + except: + return + return cr + 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: + cr = (env.get('LINES', 25), env.get('COLUMNS', 80)) + + ### Use get(key[, default]) instead of a try/catch + #try: + # cr = (env['LINES'], env['COLUMNS']) + #except: + # cr = (25, 80) + return int(cr[1]), int(cr[0]) + +def cap_text(text, length): + """Cap text length to length + + """ + if len(text) > length: + return text[0:length-1] + "…" + else: + return text + +def format_percent(percent): + """Formatte les pourcentages en ajoutant des espaces si + nécessaire. + + """ + + if percent < 10: + return " %s" % (percent,) + elif percent < 100: + return " %s" % (percent,) + else: + return str(percent) + +def rojv(percent, seuils=(100, 75, 50, 25), couls=("cyan", "vert", "jaune", "orange", "rouge")): + """Retourne la couleur qui va bien avec le pourcentage + + Les pourcentages seuil, et les couleurs associées, doivent être + rangés en ordre décroissant. + + """ + lens = len(seuils) + lenc = len(couls) + if lens+1 == lenc: + coul = couls[0] + for i in range(lens): + if percent < seuils[i]: + coul = couls[i+1] + else: + raise EnvironmentError("Seuils doit contenir une variable de moins par rapport à couls") + return coul + +class Animation(object): + """Propose une animation lors de la mise à jour ou de + l'évolution d'une tâche. + + Si le nombre de cycles est défini, affiche un pourcentage + d'évolution. + + Si l'option kikoo est définit, met une jauge en plus. + + Sinon, affiche un truc qui tourne. + + """ + + def __init__(self, texte, nb_cycles=0, couleur=True, kikoo=True): + """__init__ + + """ + self.texte = texte + self.nb_cycles = nb_cycles + self.kikoo = kikoo + self.step = 0 + self.couleur = couleur + + def kikoo(self, kikoo): + """Switch pour la valeur kikoo""" + self.kikoo = kikoo + + def new_step(self): + """Effectue une étape dans l'affichage + + """ + cols, _ = getTerminalSize() + + if not self.nb_cycles > 0: + sys.stdout.write("\r%s ..... %s" % (cap_text(self.texte, cols - 10), "\|/-"[self.step%4])) + else: + percent = int((self.step+1.0)/self.nb_cycles*100) + if not self.kikoo or cols <= 55: + if self.couleur: + percent = style("%s %%" % (format_percent(percent),), rojv(percent)) + else: + percent = "%s %%" % (format_percent(percent),) + sys.stdout.write("\r%s : %s" % (cap_text(self.texte, cols - 10), percent)) + else: + # La kikoo bar est une barre de la forme [###### ] + # Nombre de # + amount = percent/4 + # On remplit le reste avec des espaces. + if self.couleur: + # kikoo_print contient la barre et le pourcentage, colorés en fonction du pourcentage + kikoo_print = style("[%s%s%s] %s %%" % (amount * '=', + ">", + (25-amount) * ' ', + format_percent(percent)), + rojv(percent)) + + else: + kikoo_print = "[%s%s%s] %s %%" % (amount * '=', + ">", + (25-amount) * ' ', + format_percent(percent)) + sys.stdout.write("\r%s %s" % (cap_text(self.texte, cols - 45), + kikoo_print)) + sys.stdout.flush() + self.step += 1 + + def end(self): + """Prints a line return + + """ + sys.stdout.write("\n") + sys.stdout.flush() + +@static_var("styles", {}) +def style(texte, what=[]): + """ + Pretty text is pretty + On peut appliquer plusieurs styles d'affilée, ils seront alors traités + de gauche à droite (les plus à droite sont donc ceux appliqués en dernier, + et les plus à gauche en premier, ce qui veut dire que les plus à droite + viennent après, et non avant, de fait, comme ils arrivent après, ils + n'arrivent pas avant, et du coup, ceux qui arrivent avant peuvent être + écrasés par ceux qui arrivent après… + + Tout ça pour dire que style(texte, ['vert', 'blanc', 'bleu', 'kaki']) est + équivalent à style(texte, ['kaki']) qui équivaut à + KeyError Traceback (most recent call last) + in () + ----> 1 styles['kaki'] + + KeyError: 'kaki' + Sinon, il est possible de changer la couleur de fond grace aux couleur + f_, et de mettre du GRAS (je songe encore à un module qui fasse du + UPPER, parce que c'est inutile, donc kewl. + + """ + if isinstance(what, str): + what = [what] + # Pour l'éventuelle coloration du fond. + add = 0 + + if not what: + return texte + + if style.styles == {}: + zeros = {'noir' : 30, + 'rougefonce': 31, + 'vertfonce' : 32, + 'orange' : 33, + 'bleufonce' : 34, + 'violet' : 35, + 'cyanfonce' : 36, + 'grisclair' : 37, + } + f_zeros = { "f_"+coul : val+10 for (coul, val) in zeros.iteritems() } + zeros.update(f_zeros) + zeros = { coul: "0;%s" % (val,) for (coul, val) in zeros.items() } + + ones = { 'gris': 30, + 'rouge': 31, + 'vert': 32, + 'jaune': 33, + 'bleu': 34, + 'magenta': 35, + 'cyan': 36, + 'blanc': 37, + 'gras': 50, + } + f_ones = { "f_"+coul : val+10 for (coul, val) in ones.iteritems() } + ones.update(f_ones) + ones = { coul: "1;%s" % (val,) for (coul, val) in ones.items() } + + style.styles.update(zeros) + style.styles.update(ones) + for element in what: + texte = "\033[%sm%s\033[1;0m" % (style.styles[element], texte) + return texte + +# WARNING! TOO MANY UPPER CASES. +OK = style('Ok', 'vert') +WARNING = style('Warning', 'jaune') +ERREUR = style('Erreur', 'rouge') + +def prettyDoin(what, status): + """Affiche une opération en cours et met son statut à jour + + """ + if status == "...": + sys.stdout.write("\r[%s] %s" % (style(status, "jaune"), what)) + elif status == "Ok": + sys.stdout.write("\r[%s] %s\n" % (OK, what)) + elif status == "Warning": + sys.stdout.write("\r[%s] %s\n" % (WARNING, what)) + else: + sys.stdout.write("\r[%s] %s\n" % (ERREUR, what)) + sys.stdout.flush() + +if __name__ == "__main__": + import time + a = Animation(texte="Test de l'animation", nb_cycles=10000, couleur=True, kikoo=True) + for i in range(0, a.nb_cycles): + time.sleep(0.0001) + a.new_step() + a.end() + prettyDoin("Je cuis des carottes.", "...") + time.sleep(1) + prettyDoin("Les carottes sont cuites." , "Ok")