diff --git a/gestion/affichage.py b/gestion/affichage.py index 0042186f..6bae7cf8 100755 --- a/gestion/affichage.py +++ b/gestion/affichage.py @@ -15,8 +15,8 @@ # Fonctions : # getTerminalSize(), une fonction qui récupère le couple # largeur, hauteur du terminal courant. -# cap_text(text, length), un raccourci pour couper un texte -# trop long en mettant … +# cap_text(data, length, dialog=False), un raccourci pour couper un texte +# trop long en mettant * # format_percent(percent), un raccourci qui fait en sorte # qu'un pourcentage prenne toujours autant de caractères # en ajoutant des espaces (c'est pour les prints dynamiques) @@ -32,6 +32,8 @@ # Classes : # Animation, une classe générant les animations type "progress bar". +from __future__ import unicode_literals + import sys import os import fcntl @@ -41,17 +43,59 @@ import functools import time import re -oct_names = ["Pio", "Tio", "Gio", "Mio", "Kio"] -oct_sizes = [1024**(len(oct_names) - i) for i in xrange(0, len(oct_names))] -term_format = '\x1b\[[0-1];([0-9]|[0-9][0-9])m' -dialog_format = '\\\Z.' -sep_col = u"|" +from locale import getpreferredencoding + +OCT_NAMES = ["Pio", "Tio", "Gio", "Mio", "Kio"] +OCT_SIZES = [1024**(len(OCT_NAMES) - i) for i in xrange(0, len(OCT_NAMES))] +TERM_FORMAT = '\x1b\[[0-1];([0-9]|[0-9][0-9])m' +DIALOG_FORMAT = '\\\Z.' +SEP_COL = "|" + +def try_decode(string): + """Essaye de décoder dans les deux formats plausibles qu'on peut + avoir en réception. + + """ + unicode_str = "" + try: + return string.decode("UTF-8") + except UnicodeDecodeError: + pass + + try: + return string.decode("ISO-8859-15") + except: + pass + +def guess_preferred_encoding(): + """On essaye de deviner l'encodage favori de l'utilisateur. + C'est inspiré d'affich_tools. + + Il faudrait l'améliorer si nécessaire. + + """ + encoding = None + try: + encoding = getpreferredencoding() + except: + pass + + if not encoding: + encoding = sys.stdin.encoding or 'UTF-8' + + if encoding == "ANSI_X3.4-1968": + encoding = 'UTF-8' + + return encoding def static_var(couples): """Decorator setting static variable to a function. """ + # Using setattr magic, we set static + # variable on function. This avoid + # computing stuff again. def decorate(fun): functools.wraps(fun) for (name, val) in couples: @@ -67,14 +111,24 @@ def getTerminalSize(): portable. """ - env = os.environ + def ioctl_GWINSZ(fd): try: - cr = struct.unpack('hh', fcntl.ioctl(fd, termios.TIOCGWINSZ, '1234')) + # unpack C structure, the first argument says that we will get two short + # integers (h). + # ioctl is a C function which do an operation on a file descriptor, the operation + # here is termios.TIOCGWINSZ which will get the return to be term size. The third + # argument is a buffer passed to ioctl. Its size is used to define the size of the + # the return + cr = struct.unpack(b'hh', fcntl.ioctl(fd, termios.TIOCGWINSZ, b'1234')) except: return return cr + + # First, we use this magic function on stdin/stdout/stderr. cr = ioctl_GWINSZ(0) or ioctl_GWINSZ(1) or ioctl_GWINSZ(2) + + # If that didn't work, we try to do this on a custom file descriptor. if not cr: try: fd = os.open(os.ctermid(), os.O_RDONLY) @@ -82,25 +136,12 @@ def getTerminalSize(): os.close(fd) except: pass + + # If that failed, we use os.environ, and if that fails, we set default values. 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) + cr = (os.environ.get('LINES', 25), os.environ.get('COLUMNS', 80)) return int(cr[1]), int(cr[0]) -def cap_text(text, length): - """Cap text length to length-1 - - """ - 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. @@ -123,11 +164,11 @@ def rojv(percent, seuils=(100, 75, 50, 25), couls=("cyan", "vert", "jaune", "ora """ lens = len(seuils) lenc = len(couls) - if lens+1 == lenc: + if lens + 1 == lenc: coul = couls[0] for i in range(lens): if percent < seuils[i]: - coul = couls[i+1] + coul = couls[i + 1] else: raise EnvironmentError("Seuils doit contenir une variable de moins par rapport à couls") return coul @@ -169,21 +210,37 @@ class Animation(object): self.beginTime = time.time() cols, _ = getTerminalSize() + # La seule façon efficace de faire de l'affichage de barres de chargement ou autres dynamiquement + # est d'utiliser sys.stdout. C'est pas super propre, mais de toute façon, les trucs kikoo c'est + # rarement propre, d'abord. + + # Si le nombre de cycles est indéfini, on affiche une "barre de chargement" non bornée. if not self.nb_cycles > 0: - sys.stdout.write("\r%s ..... %s" % (cap_text(self.texte, cols - 10), "\|/-"[self.step%4])) + proceeding = "\r%s ..... %s" % (cap_text(self.texte, cols - 10), "\|/-"[self.step % 4]) + sys.stdout.write(proceeding.encode(guess_preferred_encoding())) + + # Sinon, on affiche un truc avec une progression/un pourcentage else: - percent = int((self.step+1.0)/self.nb_cycles*100) + percent = int((self.step + 1.0)/self.nb_cycles * 100) + + # Quand on manque de colonnes, on évite les trucs trop verbeux, idem si la kikooness + # n'est pas demandée. if not self.kikoo or cols <= 55: if self.couleur: + # Avec de la couleur. percent = style("%s %%" % (format_percent(percent),), rojv(percent)) else: + # Sans couleur (so sad) percent = "%s %%" % (format_percent(percent),) - sys.stdout.write("\r%s : %s" % (cap_text(self.texte, cols - 10), percent)) + proceeding = "\r%s : %s" % (cap_text(self.texte, cols - 10), percent) + sys.stdout.write(proceeding.encode(guess_preferred_encoding())) + + # Du kikoo ! Du kikoo ! 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 * '=', @@ -197,34 +254,31 @@ class Animation(object): ">", (25-amount) * ' ', format_percent(percent)) - sys.stdout.write("\r%s %s" % (cap_text(self.texte, cols - 45), - kikoo_print)) + proceeding = "\r%s %s" % (cap_text(self.texte, cols - 45), kikoo_print) + sys.stdout.write(proceeding.encode(guess_preferred_encoding())) + # What if we add more kikoo? if self.timer: self.elapsedTime = time.time() - self.beginTime - sys.stdout.write(" (temps écoulé : %ds)" % (self.elapsedTime)) + proceeding = " (temps écoulé : %ds)" % (self.elapsedTime) + sys.stdout.write(proceeding.encode(guess_preferred_encoding())) sys.stdout.flush() self.step += 1 def end(self): - """Prints a line return - - """ - sys.stdout.write("\n") + """Prints a line return""" + sys.stdout.write("\n".encode(guess_preferred_encoding())) sys.stdout.flush() def nostyle(dialog=False): - """ - Annule tout style précédent. - """ + """Annule tout style précédent.""" if dialog: return "\Zn" return "\033[1;0m" @static_var([("styles", {})]) def style(texte, what=None, dialog=False): - """ - Pretty text is pretty + """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 @@ -250,36 +304,47 @@ def style(texte, what=None, dialog=False): if dialog: return dialogStyle(texte, what) - if isinstance(what, str): + if isinstance(what, unicode): what = [what] + if isinstance(what, str): + what = [try_decode(what)] if not what: return texte + # Si la variable statique styles n'est pas peuplée… if style.styles == {}: - zeros = {'noir' : 30, - 'rougefonce': 31, - 'vertfonce' : 32, - 'orange' : 33, - 'bleufonce' : 34, - 'violet' : 35, - 'cyanfonce' : 36, - 'grisclair' : 37, - } + zeros = { + 'noir' : 30, + 'rougefonce': 31, + 'vertfonce' : 32, + 'orange' : 33, + 'bleufonce' : 34, + 'violet' : 35, + 'cyanfonce' : 36, + 'grisclair' : 37, + } + + # Méthode "automatisée" pour remplir la version "background" des codes + # couleur du shell. 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, - } + # Plus de couleurs (les codes sont de la forme \033[0;blah ou \033[1;blah, donc + # ici on remplit la partie 1;. + 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() } @@ -287,11 +352,11 @@ def style(texte, what=None, dialog=False): style.styles.update(zeros) style.styles.update(ones) style.styles["none"] = "1;0" + 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') @@ -308,15 +373,16 @@ def dialogStyle(texte, what): what = [what] dialog_styles = { - 'rouge': 1, - 'orange': 1, - 'vert': 2, - 'jaune': 3, - 'bleu': 4, - 'violet': 5, - 'cyan': 6, - 'gris': 0, - 'gras': 'b' } + 'rouge': 1, + 'orange': 1, + 'vert': 2, + 'jaune': 3, + 'bleu': 4, + 'violet': 5, + 'cyan': 6, + 'gris': 0, + 'gras': 'b', + } for elem in what: texte = "\Z%s%s\Zn" % (dialog_styles[elem], texte) @@ -336,30 +402,30 @@ def prettyDoin(what, status): sys.stdout.write("\r[%s] %s\n" % (ERREUR, what)) sys.stdout.flush() -def tronque(data, largeur, dialog): - """ - Tronque une chaîne à une certaine longueur en excluant +def cap_text(data, length, dialog=False): + """Tronque une chaîne à une certaine longueur en excluant les commandes de style. + """ # découpage d'une chaine trop longue - regexp = re.compile(dialog_format if dialog else term_format) - new_data = u'' + regexp = re.compile(DIALOG_FORMAT if dialog else TERM_FORMAT) + new_data = '' new_len = 0 # On laisse la mise en forme et on coupe les caratères affichés while True : s = regexp.search(data) if s: - if s.start() + new_len > largeur: - new_data += data[:largeur - new_len - 1] + nostyle() + '*' + if s.start() + new_len > length: + new_data += data[:length - new_len - 1] + nostyle() + '*' break else: new_data += data[:s.end()] data = data[s.end():] new_len += s.start() else: - if new_len + len(data) > largeur: - new_data += data[:largeur - new_len - 1] + '*' + if new_len + len(data) > length: + new_data += data[:length - new_len - 1] + '*' data = "" else: new_data += data @@ -371,23 +437,23 @@ def tronque(data, largeur, dialog): def aligne(data, alignement, largeur, dialog) : # Longeur sans les chaines de formatage - longueur = len(re.sub(dialog_format if dialog else term_format, '', data)) + longueur = len(re.sub(DIALOG_FORMAT if dialog else TERM_FORMAT, '', data)) # Alignement if longueur > largeur: - return tronque(data, largeur, dialog) + return cap_text(data, largeur, dialog) elif longueur == largeur : return data elif alignement == 'g': - return u' ' + data + u' '*(largeur-longueur-1) + return ' ' + data + ' '*(largeur-longueur-1) elif alignement == 'd': - return u' '*(largeur-longueur-1) + data + u' ' + return ' '*(largeur-longueur-1) + data + ' ' else: - return u' '*((largeur-longueur)/2) + data + u' '*((largeur-longueur+1)/2) + return ' '*((largeur-longueur)/2) + data + ' '*((largeur-longueur+1)/2) def format_data(data, format): if format == 's': @@ -395,9 +461,9 @@ def format_data(data, format): elif format == 'o': data = float(data) - for i in xrange(0, len(oct_names)): - if data > oct_sizes[i]: - return "%.2f %s" % (data/oct_sizes[i], oct_names[i]) + for i in xrange(0, len(OCT_NAMES)): + if data > OCT_SIZES[i]: + return "%.2f %s" % (data/OCT_SIZES[i], OCT_NAMES[i]) return "%.0f o" % (data) def tableau(data, titre=None, largeur=None, alignement=None, format=None, dialog=False, width=None, styles=None, swap=None): @@ -434,7 +500,7 @@ def tableau(data, titre=None, largeur=None, alignement=None, format=None, dialog if data and isinstance(data, list): nb_cols = len(data[0]) else: - return u"" + return "" if format is None: format = ['s'] * nb_cols @@ -449,8 +515,8 @@ def tableau(data, titre=None, largeur=None, alignement=None, format=None, dialog data = [[format_data(ligne[i], format[i]) for i in xrange(nb_cols)] for ligne in data] - if not largeur : - largeur = [max([len(re.sub(dialog_format if dialog else term_format, '', ligne[i])) for ligne in data]) for i in range(nb_cols)] + if not largeur: + largeur = [max([len(re.sub(DIALOG_FORMAT if dialog else TERM_FORMAT, '', ligne[i])) for ligne in data]) for i in range(nb_cols)] elif '*' in largeur or -1 in largeur: for i in xrange(0, len(largeur)): if largeur[i] is not None: @@ -477,22 +543,22 @@ def tableau(data, titre=None, largeur=None, alignement=None, format=None, dialog ########## if titre : # ligne de titre - chaine = sep_col + sep_col.join([style(aligne(titre[i], 'c', largeur[i], dialog), styles[i]) for i in range(nb_cols)]) + sep_col + u'\n' + chaine = SEP_COL + SEP_COL.join([style(aligne(titre[i], 'c', largeur[i], dialog), styles[i]) for i in range(nb_cols)]) + SEP_COL + '\n' # ligne de séparation - chaine += sep_col + u'+'.join([u'-'*largeur[i] for i in range(nb_cols)]) + sep_col + u'\n' + chaine += SEP_COL + '+'.join(['-'*largeur[i] for i in range(nb_cols)]) + SEP_COL + '\n' else : - chaine = u'' + chaine = '' # Les données ############# j = 0 for ligne in data: - chaine += u"%s" % (sep_col,) + chaine += "%s" % (SEP_COL,) if swap: - chaine += sep_col.join([style(ligne[i], swap[j]) for i in range(nb_cols)]) + chaine += SEP_COL.join([style(ligne[i], swap[j]) for i in range(nb_cols)]) else: - chaine += sep_col.join([style(ligne[i], styles[i]) for i in range(nb_cols)]) - chaine += u"%s\n" % (sep_col,) + chaine += SEP_COL.join([style(ligne[i], styles[i]) for i in range(nb_cols)]) + chaine += "%s\n" % (SEP_COL,) j = (j + 1) % len(swap) return chaine @@ -508,7 +574,7 @@ if __name__ == "__main__": time.sleep(1) prettyDoin("Les carottes sont cuites." , "Ok") - data = [[style("Durand", "rouge"), u"Toto", u"40", u"50 rue caca"], [u"Dupont", "Robert", u"50", "42" + style(" avenue ", "vert") + style("dumotel", 'rouge')], [style("znvuzbvzruobouzb", ["gras", "vert"]), u"pppoe", u"1", u"poiodur 50 pepe"]] - titres = (u"Nom", u"Prénom", u"Âge", u"Adresse") + data = [[style("Durand", "rouge"), "Toto", "40", "50 rue Döp"], ["Dupont", "Robert", "50", "42" + style(" avenue ", "vert") + style("dumotel", 'rouge')], [style("znvuzbvzruobouzb", ["gras", "vert"]), "pppoe", "1", "poiodur 50 pepe"]] + titres = ("Nom", "Prénom", "Âge", "Adresse") longueurs = [25, 25, '*', '*'] - print tableau(data, titres, longueurs) + print tableau(data, titres, longueurs).encode(guess_preferred_encoding())