scripts/gestion/affichage.py
2014-08-25 20:40:17 +02:00

486 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"]
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"|"
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,
'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)
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])
return "%.0f o" % (data)
def tableau(data, titre=None, largeur=None, alignement=None, format=None, dialog=False, width=None, styles=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
if styles is None:
styles = [None] * 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([style(aligne(titre[i], 'c', largeur[i], dialog), styles[i]) 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([style(ligne[i], styles[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)