481 lines
15 KiB
Python
Executable file
481 lines
15 KiB
Python
Executable file
#!/usr/bin/env python
|
|
# -*- coding: utf-8 -*-
|
|
#
|
|
# Vrac d'outils pour avoir un affichage sympathique.
|
|
# Auteur : Pierre-Elliott Bécue <becue@crans.org>
|
|
# Licence : GPLv3
|
|
#
|
|
# Contenu :
|
|
# ---------
|
|
#
|
|
# Décorateur :
|
|
# static_var(name, val), un décorateur pour créer une variable
|
|
# statique dans une fonction
|
|
#
|
|
# 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 …
|
|
# 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)
|
|
# rojv(percent, seuils, couls), une méthode qui affiche une couleur
|
|
# pour un pourcentage en fonction de la valeur de celui-ci, des
|
|
# seuils définis, et des couleurs disponibles.
|
|
# style(texte, what), une méthode qui applique un ensemble de styles
|
|
# contenus dans what au texte texte.
|
|
# dialogStyle(texte, what), pareil, mais dans dialog
|
|
# prettyDoin(what, status), une méthode qui affiche des messages "service"-
|
|
# like, en mettant le status en fonction de l'accomplissement de la tâche.
|
|
#
|
|
# Classes :
|
|
# Animation, une classe générant les animations type "progress bar".
|
|
|
|
import sys
|
|
import os
|
|
import fcntl
|
|
import termios
|
|
import struct
|
|
import functools
|
|
import time
|
|
import re
|
|
|
|
oct_names = ["Pio", "Tio", "Gio", "Mio", "Kio", "o"]
|
|
oct_sizes = [1024**(len(oct_names) - i - 1) 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"|"
|
|
|
|
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-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.
|
|
|
|
"""
|
|
|
|
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, timer=True):
|
|
"""__init__
|
|
|
|
"""
|
|
self.texte = texte
|
|
self.nb_cycles = nb_cycles
|
|
self.kikoo = kikoo
|
|
self.step = 0
|
|
self.couleur = couleur
|
|
self.timer = timer
|
|
self.beginTime = 0
|
|
|
|
def kikoo(self, kikoo):
|
|
"""Switch pour la valeur kikoo"""
|
|
self.kikoo = kikoo
|
|
|
|
def new_step(self):
|
|
"""Effectue une étape dans l'affichage
|
|
|
|
"""
|
|
if self.step == 0 and self.timer:
|
|
self.beginTime = time.time()
|
|
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))
|
|
|
|
if self.timer:
|
|
self.elapsedTime = time.time() - self.beginTime
|
|
sys.stdout.write(" (temps écoulé : %ds)" % (self.elapsedTime))
|
|
sys.stdout.flush()
|
|
self.step += 1
|
|
|
|
def end(self):
|
|
"""Prints a line return
|
|
|
|
"""
|
|
sys.stdout.write("\n")
|
|
sys.stdout.flush()
|
|
|
|
def nostyle(dialog=False):
|
|
"""
|
|
Annule tout style précédent.
|
|
"""
|
|
if dialog:
|
|
return "\Zn"
|
|
return "\033[1;0m"
|
|
|
|
@static_var("styles", {})
|
|
def style(texte, what=[], dialog=False):
|
|
"""
|
|
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)
|
|
<ipython-input-2-d9c32f2123f0> in <module>()
|
|
----> 1 styles['kaki']
|
|
|
|
KeyError: 'kaki'
|
|
Sinon, il est possible de changer la couleur de fond grace aux couleur
|
|
f_<couleur>, et de mettre du GRAS (je songe encore à un module qui fasse du
|
|
UPPER, parce que c'est inutile, donc kewl.
|
|
|
|
"""
|
|
if dialog:
|
|
return dialogStyle(texte, what)
|
|
|
|
if isinstance(what, str):
|
|
what = [what]
|
|
|
|
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)
|
|
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')
|
|
|
|
def dialogStyle(texte, what):
|
|
"""Fournit du texte coloré pour dialog.
|
|
|
|
Séparé volontairement de style pour une meilleure
|
|
utilisation.
|
|
|
|
"""
|
|
|
|
if isinstance(what, str):
|
|
what = [what]
|
|
|
|
dialog_styles = {
|
|
'rouge': 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)
|
|
return texte
|
|
|
|
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()
|
|
|
|
def tronque(data, largeur, dialog):
|
|
"""
|
|
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''
|
|
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() + '*'
|
|
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] + '*'
|
|
data = ""
|
|
else:
|
|
new_data += data
|
|
data = ""
|
|
|
|
if not data:
|
|
break
|
|
return new_data
|
|
|
|
def aligne(data, alignement, largeur, dialog) :
|
|
# Longeur sans les chaines de formatage
|
|
longueur = len(re.sub(dialog_format if dialog else term_format, '', data))
|
|
|
|
# Alignement
|
|
if longueur > largeur:
|
|
return tronque(data, largeur, dialog)
|
|
|
|
elif longueur == largeur :
|
|
return data
|
|
|
|
elif alignement == 'g':
|
|
return u' ' + data + u' '*(largeur-longueur-1)
|
|
|
|
elif alignement == 'd':
|
|
return u' '*(largeur-longueur-1) + data + u' '
|
|
|
|
else:
|
|
return u' '*((largeur-longueur)/2) + data + u' '*((largeur-longueur+1)/2)
|
|
|
|
def format_data(data, format):
|
|
if format == 's':
|
|
return unicode(data)
|
|
|
|
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])
|
|
|
|
def tableau(data, titre=None, largeur=None, alignement=None, format=None, dialog=False, width=None):
|
|
"""
|
|
Retourne une chaine formatée repésentant un tableau.
|
|
|
|
data : liste de listes, chacune contenant les valeurs d'une ligne
|
|
|
|
titre : liste des titres
|
|
Si none, n'affiche pas de ligne de titre
|
|
|
|
largeur : liste des largeurs des colonnes, '*' met la plus grande
|
|
largeur possible.
|
|
Si None, réduit aux max chaque colonne
|
|
|
|
alignement : liste des alignements : c = centrer
|
|
g = gauche
|
|
d = droit
|
|
Si None, met c pour chaque colonne
|
|
|
|
format : liste des formats : s = string
|
|
o = octet
|
|
Si None, s pour chaque colonne
|
|
width : force la largeur
|
|
"""
|
|
|
|
if data and isinstance(data, list):
|
|
nb_cols = len(data[0])
|
|
else:
|
|
return u""
|
|
|
|
if format is None:
|
|
format = ['s'] * nb_cols
|
|
|
|
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)]
|
|
elif '*' in largeur or -1 in largeur:
|
|
sum_larg = sum([l for l in largeur if l != '*'])
|
|
if width:
|
|
cols = width
|
|
else:
|
|
cols, rows = getTerminalSize()
|
|
if dialog:
|
|
cols = cols - 6
|
|
stars = largeur.count('*') + largeur.count(-1)
|
|
for i in range(nb_cols):
|
|
if largeur[i] in ['*', -1] :
|
|
largeur[i] = max(cols - sum_larg - nb_cols - 1, 3)/stars
|
|
|
|
if alignement is None:
|
|
alignement = ['c'] * nb_cols
|
|
|
|
data = [[aligne(ligne[i], alignement[i], largeur[i], dialog) for i in range(nb_cols)] for ligne in data]
|
|
|
|
# Le titre
|
|
##########
|
|
if titre :
|
|
# ligne de titre
|
|
chaine = sep_col + sep_col.join([aligne(titre[i], 'c', largeur[i], dialog) for i in range(nb_cols)]) + sep_col + u'\n'
|
|
# ligne de séparation
|
|
chaine += sep_col + u'+'.join([u'-'*largeur[i] for i in range(nb_cols)]) + sep_col + u'\n'
|
|
else :
|
|
chaine = u''
|
|
|
|
# Les données
|
|
#############
|
|
chaine += u'\n'.join([sep_col + sep_col.join([ligne[i] for i in range(nb_cols)]) + sep_col for ligne in data])
|
|
|
|
return chaine
|
|
|
|
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")
|
|
|
|
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")
|
|
longueurs = [25, 25, '*', '*']
|
|
print tableau(data, titres, longueurs)
|