Ajout de fonctionnalités, et passage en "tout unicode".

This commit is contained in:
Pierre-Elliott Bécue 2015-02-15 18:29:07 +01:00
parent 491ea7fa39
commit 80874c7a8f

View file

@ -15,8 +15,8 @@
# Fonctions : # Fonctions :
# getTerminalSize(), une fonction qui récupère le couple # getTerminalSize(), une fonction qui récupère le couple
# largeur, hauteur du terminal courant. # largeur, hauteur du terminal courant.
# cap_text(text, length), un raccourci pour couper un texte # cap_text(data, length, dialog=False), un raccourci pour couper un texte
# trop long en mettant # trop long en mettant *
# format_percent(percent), un raccourci qui fait en sorte # format_percent(percent), un raccourci qui fait en sorte
# qu'un pourcentage prenne toujours autant de caractères # qu'un pourcentage prenne toujours autant de caractères
# en ajoutant des espaces (c'est pour les prints dynamiques) # en ajoutant des espaces (c'est pour les prints dynamiques)
@ -32,6 +32,8 @@
# Classes : # Classes :
# Animation, une classe générant les animations type "progress bar". # Animation, une classe générant les animations type "progress bar".
from __future__ import unicode_literals
import sys import sys
import os import os
import fcntl import fcntl
@ -41,17 +43,59 @@ import functools
import time import time
import re import re
oct_names = ["Pio", "Tio", "Gio", "Mio", "Kio"] from locale import getpreferredencoding
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' OCT_NAMES = ["Pio", "Tio", "Gio", "Mio", "Kio"]
dialog_format = '\\\Z.' OCT_SIZES = [1024**(len(OCT_NAMES) - i) for i in xrange(0, len(OCT_NAMES))]
sep_col = u"|" 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): def static_var(couples):
"""Decorator setting static variable """Decorator setting static variable
to a function. to a function.
""" """
# Using setattr magic, we set static
# variable on function. This avoid
# computing stuff again.
def decorate(fun): def decorate(fun):
functools.wraps(fun) functools.wraps(fun)
for (name, val) in couples: for (name, val) in couples:
@ -67,14 +111,24 @@ def getTerminalSize():
portable. portable.
""" """
env = os.environ
def ioctl_GWINSZ(fd): def ioctl_GWINSZ(fd):
try: 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: except:
return return
return cr return cr
# First, we use this magic function on stdin/stdout/stderr.
cr = ioctl_GWINSZ(0) or ioctl_GWINSZ(1) or ioctl_GWINSZ(2) 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: if not cr:
try: try:
fd = os.open(os.ctermid(), os.O_RDONLY) fd = os.open(os.ctermid(), os.O_RDONLY)
@ -82,25 +136,12 @@ def getTerminalSize():
os.close(fd) os.close(fd)
except: except:
pass pass
# If that failed, we use os.environ, and if that fails, we set default values.
if not cr: if not cr:
cr = (env.get('LINES', 25), env.get('COLUMNS', 80)) cr = (os.environ.get('LINES', 25), os.environ.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]) 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): def format_percent(percent):
"""Formatte les pourcentages en ajoutant des espaces si """Formatte les pourcentages en ajoutant des espaces si
nécessaire. nécessaire.
@ -169,21 +210,37 @@ class Animation(object):
self.beginTime = time.time() self.beginTime = time.time()
cols, _ = getTerminalSize() 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: 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: 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 not self.kikoo or cols <= 55:
if self.couleur: if self.couleur:
# Avec de la couleur.
percent = style("%s %%" % (format_percent(percent),), rojv(percent)) percent = style("%s %%" % (format_percent(percent),), rojv(percent))
else: else:
# Sans couleur (so sad)
percent = "%s %%" % (format_percent(percent),) 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: else:
# La kikoo bar est une barre de la forme [======> ] # La kikoo bar est une barre de la forme [======> ]
# Nombre de = # Nombre de =
amount = percent/4 amount = percent/4
# On remplit le reste avec des espaces.
if self.couleur: if self.couleur:
# kikoo_print contient la barre et le pourcentage, colorés en fonction du pourcentage # kikoo_print contient la barre et le pourcentage, colorés en fonction du pourcentage
kikoo_print = style("[%s%s%s] %s %%" % (amount * '=', kikoo_print = style("[%s%s%s] %s %%" % (amount * '=',
@ -197,34 +254,31 @@ class Animation(object):
">", ">",
(25-amount) * ' ', (25-amount) * ' ',
format_percent(percent)) format_percent(percent))
sys.stdout.write("\r%s %s" % (cap_text(self.texte, cols - 45), proceeding = "\r%s %s" % (cap_text(self.texte, cols - 45), kikoo_print)
kikoo_print)) sys.stdout.write(proceeding.encode(guess_preferred_encoding()))
# What if we add more kikoo?
if self.timer: if self.timer:
self.elapsedTime = time.time() - self.beginTime 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() sys.stdout.flush()
self.step += 1 self.step += 1
def end(self): def end(self):
"""Prints a line return """Prints a line return"""
sys.stdout.write("\n".encode(guess_preferred_encoding()))
"""
sys.stdout.write("\n")
sys.stdout.flush() sys.stdout.flush()
def nostyle(dialog=False): def nostyle(dialog=False):
""" """Annule tout style précédent."""
Annule tout style précédent.
"""
if dialog: if dialog:
return "\Zn" return "\Zn"
return "\033[1;0m" return "\033[1;0m"
@static_var([("styles", {})]) @static_var([("styles", {})])
def style(texte, what=None, dialog=False): 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 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, 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 et les plus à gauche en premier, ce qui veut dire que les plus à droite
@ -250,14 +304,18 @@ def style(texte, what=None, dialog=False):
if dialog: if dialog:
return dialogStyle(texte, what) return dialogStyle(texte, what)
if isinstance(what, str): if isinstance(what, unicode):
what = [what] what = [what]
if isinstance(what, str):
what = [try_decode(what)]
if not what: if not what:
return texte return texte
# Si la variable statique styles n'est pas peuplée…
if style.styles == {}: if style.styles == {}:
zeros = {'noir' : 30, zeros = {
'noir' : 30,
'rougefonce': 31, 'rougefonce': 31,
'vertfonce' : 32, 'vertfonce' : 32,
'orange' : 33, 'orange' : 33,
@ -266,11 +324,17 @@ def style(texte, what=None, dialog=False):
'cyanfonce' : 36, 'cyanfonce' : 36,
'grisclair' : 37, '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() } f_zeros = { "f_"+coul : val+10 for (coul, val) in zeros.iteritems() }
zeros.update(f_zeros) zeros.update(f_zeros)
zeros = { coul: "0;%s" % (val,) for (coul, val) in zeros.items() } zeros = { coul: "0;%s" % (val,) for (coul, val) in zeros.items() }
ones = { 'gris': 30, # 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, 'rouge': 31,
'vert': 32, 'vert': 32,
'jaune': 33, 'jaune': 33,
@ -280,6 +344,7 @@ def style(texte, what=None, dialog=False):
'blanc': 37, 'blanc': 37,
'gras': 50, 'gras': 50,
} }
f_ones = { "f_"+coul : val+10 for (coul, val) in ones.iteritems() } f_ones = { "f_"+coul : val+10 for (coul, val) in ones.iteritems() }
ones.update(f_ones) ones.update(f_ones)
ones = { coul: "1;%s" % (val,) for (coul, val) in ones.items() } 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(zeros)
style.styles.update(ones) style.styles.update(ones)
style.styles["none"] = "1;0" style.styles["none"] = "1;0"
for element in what: for element in what:
texte = "\033[%sm%s\033[1;0m" % (style.styles[element], texte) texte = "\033[%sm%s\033[1;0m" % (style.styles[element], texte)
return texte return texte
# WARNING! TOO MANY UPPER CASES.
OK = style('Ok', 'vert') OK = style('Ok', 'vert')
WARNING = style('Warning', 'jaune') WARNING = style('Warning', 'jaune')
ERREUR = style('Erreur', 'rouge') ERREUR = style('Erreur', 'rouge')
@ -316,7 +381,8 @@ def dialogStyle(texte, what):
'violet': 5, 'violet': 5,
'cyan': 6, 'cyan': 6,
'gris': 0, 'gris': 0,
'gras': 'b' } 'gras': 'b',
}
for elem in what: for elem in what:
texte = "\Z%s%s\Zn" % (dialog_styles[elem], texte) 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.write("\r[%s] %s\n" % (ERREUR, what))
sys.stdout.flush() sys.stdout.flush()
def tronque(data, largeur, dialog): def cap_text(data, length, dialog=False):
""" """Tronque une chaîne à une certaine longueur en excluant
Tronque une chaîne à une certaine longueur en excluant
les commandes de style. les commandes de style.
""" """
# découpage d'une chaine trop longue # découpage d'une chaine trop longue
regexp = re.compile(dialog_format if dialog else term_format) regexp = re.compile(DIALOG_FORMAT if dialog else TERM_FORMAT)
new_data = u'' new_data = ''
new_len = 0 new_len = 0
# On laisse la mise en forme et on coupe les caratères affichés # On laisse la mise en forme et on coupe les caratères affichés
while True : while True :
s = regexp.search(data) s = regexp.search(data)
if s: if s:
if s.start() + new_len > largeur: if s.start() + new_len > length:
new_data += data[:largeur - new_len - 1] + nostyle() + '*' new_data += data[:length - new_len - 1] + nostyle() + '*'
break break
else: else:
new_data += data[:s.end()] new_data += data[:s.end()]
data = data[s.end():] data = data[s.end():]
new_len += s.start() new_len += s.start()
else: else:
if new_len + len(data) > largeur: if new_len + len(data) > length:
new_data += data[:largeur - new_len - 1] + '*' new_data += data[:length - new_len - 1] + '*'
data = "" data = ""
else: else:
new_data += data new_data += data
@ -371,23 +437,23 @@ def tronque(data, largeur, dialog):
def aligne(data, alignement, largeur, dialog) : def aligne(data, alignement, largeur, dialog) :
# Longeur sans les chaines de formatage # 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 # Alignement
if longueur > largeur: if longueur > largeur:
return tronque(data, largeur, dialog) return cap_text(data, largeur, dialog)
elif longueur == largeur : elif longueur == largeur :
return data return data
elif alignement == 'g': elif alignement == 'g':
return u' ' + data + u' '*(largeur-longueur-1) return ' ' + data + ' '*(largeur-longueur-1)
elif alignement == 'd': elif alignement == 'd':
return u' '*(largeur-longueur-1) + data + u' ' return ' '*(largeur-longueur-1) + data + ' '
else: 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): def format_data(data, format):
if format == 's': if format == 's':
@ -395,9 +461,9 @@ def format_data(data, format):
elif format == 'o': elif format == 'o':
data = float(data) data = float(data)
for i in xrange(0, len(oct_names)): for i in xrange(0, len(OCT_NAMES)):
if data > oct_sizes[i]: if data > OCT_SIZES[i]:
return "%.2f %s" % (data/oct_sizes[i], oct_names[i]) return "%.2f %s" % (data/OCT_SIZES[i], OCT_NAMES[i])
return "%.0f o" % (data) return "%.0f o" % (data)
def tableau(data, titre=None, largeur=None, alignement=None, format=None, dialog=False, width=None, styles=None, swap=None): 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): if data and isinstance(data, list):
nb_cols = len(data[0]) nb_cols = len(data[0])
else: else:
return u"" return ""
if format is None: if format is None:
format = ['s'] * nb_cols format = ['s'] * nb_cols
@ -450,7 +516,7 @@ 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] data = [[format_data(ligne[i], format[i]) for i in xrange(nb_cols)] for ligne in data]
if not largeur: 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)] 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: elif '*' in largeur or -1 in largeur:
for i in xrange(0, len(largeur)): for i in xrange(0, len(largeur)):
if largeur[i] is not None: if largeur[i] is not None:
@ -477,22 +543,22 @@ def tableau(data, titre=None, largeur=None, alignement=None, format=None, dialog
########## ##########
if titre : if titre :
# ligne de 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 # 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 : else :
chaine = u'' chaine = ''
# Les données # Les données
############# #############
j = 0 j = 0
for ligne in data: for ligne in data:
chaine += u"%s" % (sep_col,) chaine += "%s" % (SEP_COL,)
if swap: 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: else:
chaine += sep_col.join([style(ligne[i], styles[i]) for i in range(nb_cols)]) chaine += SEP_COL.join([style(ligne[i], styles[i]) for i in range(nb_cols)])
chaine += u"%s\n" % (sep_col,) chaine += "%s\n" % (SEP_COL,)
j = (j + 1) % len(swap) j = (j + 1) % len(swap)
return chaine return chaine
@ -508,7 +574,7 @@ if __name__ == "__main__":
time.sleep(1) time.sleep(1)
prettyDoin("Les carottes sont cuites." , "Ok") 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"]] 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 = (u"Nom", u"Prénom", u"Âge", u"Adresse") titres = ("Nom", "Prénom", "Âge", "Adresse")
longueurs = [25, 25, '*', '*'] longueurs = [25, 25, '*', '*']
print tableau(data, titres, longueurs) print tableau(data, titres, longueurs).encode(guess_preferred_encoding())