423 lines
13 KiB
Python
423 lines
13 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
""" Collection de fonction/classe pour avoir un bel affichage
|
|
|
|
Copyright (C) Frédéric Pauget
|
|
Licence : GPLv2
|
|
"""
|
|
|
|
import os
|
|
import re
|
|
import shlex
|
|
import subprocess
|
|
import sys
|
|
if not "/usr/scripts" in sys.path:
|
|
sys.path.append("/usr/scripts")
|
|
import cranslib.cransstrings
|
|
|
|
# Détermination de l'encodage
|
|
ENCODING = None
|
|
try:
|
|
from locale import getpreferredencoding
|
|
ENCODING = getpreferredencoding()
|
|
except:
|
|
pass
|
|
|
|
if not ENCODING:
|
|
ENCODING = sys.stdin.encoding or 'UTF-8'
|
|
|
|
# Si aucune locale n'est définie, on se met en...
|
|
if ENCODING == "ANSI_X3.4-1968":
|
|
ENCODING = 'UTF-8'
|
|
|
|
if 'TERM' in os.environ and os.environ['TERM'] != 'unknown':
|
|
el = subprocess.Popen('tput cr ; tput el', shell=True, stdout=subprocess.PIPE).stdout.read()
|
|
else:
|
|
el = subprocess.Popen('tput -Tvt100 cr ; tput -Tvt100 el', shell=True, stdout=subprocess.PIPE).stdout.read()
|
|
|
|
try:
|
|
stdout_atty = sys.stdout.isatty()
|
|
except (IOError, AttributeError):
|
|
stdout_atty = False
|
|
|
|
def dialog(backtitle, arg, dialogrc=''):
|
|
"""
|
|
Affiche la boîte de dialogue définie avec les arguments fournis
|
|
(cf man dialog)
|
|
|
|
Paramètres:
|
|
- ``backtitle`` : (str) texte de l'en-tête, situé en haut à gauche
|
|
- ``arg`` : (str ou list) ensemble des paramètres à passer au
|
|
programme dialog
|
|
- ``dialogrc`` : (str) chemin vers le dialogrc à utiliser
|
|
|
|
Utilises:
|
|
- :py:var:`os.environ`
|
|
- :py:func:`shlex.split`
|
|
- :py:class:`subprocess.Popen`
|
|
- :py:func:`sys.exit`
|
|
|
|
Peut lever:
|
|
- :py:exc:`RuntimeError`
|
|
|
|
Retournes:
|
|
- soit un tuple dont le premier élément est le code d'erreur (0 ou 1) et
|
|
le second la sortie du programme dialog (une liste ou une liste vide)
|
|
- soit rien du tout parce que sys.exit(0)
|
|
"""
|
|
# Tant que gest_crans (au moins) n'a pas été modifié pour que arg soit une
|
|
# liste, ce kludge est nécessaire.
|
|
if not isinstance(arg, list):
|
|
arg = shlex.split(to_encoding(arg))
|
|
|
|
commande = [
|
|
"/usr/bin/dialog",
|
|
"--backtitle", backtitle,
|
|
"--cancel-label", "Retour"
|
|
] + arg
|
|
|
|
# On sort de l'unicode sandwich, il faut donc transformer en bytes encodés
|
|
# en UTF-8.
|
|
commande = [to_encoding(elem) for elem in commande]
|
|
|
|
# On copie l'environnement afin de pouvoir écraser une variable pour
|
|
# l'appel au programme dialog sans que cela affecte les autres processus.
|
|
environnement = os.environ.copy()
|
|
|
|
# Si dialog est fourni, on modifie effectivement l'environnement
|
|
if dialogrc:
|
|
environnement["DIALOGRC"] = dialogrc
|
|
|
|
# Le résultat du programme dialog est dans stderr, on la récupère dans
|
|
# un pipe
|
|
processus = subprocess.Popen(commande, stderr=subprocess.PIPE, env=environnement)
|
|
|
|
# Récupération du contenu du pipe
|
|
_, sortie = processus.communicate()
|
|
|
|
# On décode la sortie du programme dialog (et on le fait ici parce que
|
|
# c'est ici l'interface).
|
|
sortie = to_unicode(sortie)
|
|
resultat = sortie.splitlines()
|
|
|
|
# Récupération du code d'erreur
|
|
code_erreur = processus.returncode
|
|
|
|
if code_erreur == 1:
|
|
# L'utilisateur a annulé
|
|
return (1, [])
|
|
elif code_erreur == 255 and resultat:
|
|
# La liste des arguments passés au programme dialog est invalide
|
|
raise RuntimeError(arg, resultat[1].strip())
|
|
elif code_erreur == 255:
|
|
# L'utilisateur a appuyé sur ÉCHAP. Dans ce cas, on lui demande de
|
|
# confirmer.
|
|
# "0" et "0" sont là pour ajuster la taille de la fenêtre à son
|
|
# contenu
|
|
arg_confirmation = [
|
|
"--title", "Annulation",
|
|
"--yesno", "Quitter ?\nLes dernières modifications seront perdues.",
|
|
"0", "0"
|
|
]
|
|
code_confirmation, _ = dialog(backtitle, arg_confirmation, dialogrc)
|
|
if code_confirmation == 0:
|
|
# L'utilisateur veut quitter (vraiment).
|
|
sys.exit(0)
|
|
else:
|
|
# L'appui sur ÉCHAP était une fausse manip, on recommence
|
|
return dialog(backtitle, arg, dialogrc)
|
|
elif not resultat:
|
|
# Quand le code d'erreur est nul, il ne faut pas que resultat soit
|
|
# vide.
|
|
resultat = ['']
|
|
|
|
return (0, resultat)
|
|
|
|
def coul(txt, col=None, dialog=False):
|
|
"""
|
|
Retourne la chaine donnée encadrée des séquences qui
|
|
vont bien pour obtenir la couleur souhaitée
|
|
Les couleur sont celles de codecol
|
|
Il est possible de changer la couleur de fond grace aux couleur f_<couleur>
|
|
"""
|
|
if not stdout_atty or not col:
|
|
return txt
|
|
|
|
codecol = {'rouge': 31,
|
|
'vert': 32,
|
|
'jaune': 33,
|
|
'bleu': 34,
|
|
'violet': 35,
|
|
'cyan': 36,
|
|
'gris': 30,
|
|
'gras': 50}
|
|
codecol_dialog = {'rouge': 1,
|
|
'vert': 2,
|
|
'jaune': 3,
|
|
'bleu': 4,
|
|
'violet': 5,
|
|
'cyan': 6,
|
|
'gris': 0,
|
|
'gras': 'b'}
|
|
|
|
if dialog:
|
|
try:
|
|
txt = "\Z%s%s\Zn" % (codecol_dialog[col], txt)
|
|
finally:
|
|
return txt
|
|
try:
|
|
if col[:2] == 'f_':
|
|
add = 10
|
|
col = col[2:]
|
|
else:
|
|
add = 0
|
|
txt = "\033[1;%sm%s\033[1;0m" % (codecol[col] + add, txt)
|
|
finally:
|
|
return txt
|
|
|
|
OK = coul('OK', 'vert')
|
|
WARNING = coul('WARNING', 'jaune')
|
|
ERREUR = coul('ERREUR', 'rouge')
|
|
|
|
def to_unicode(txt, enc=ENCODING):
|
|
if isinstance(txt, unicode):
|
|
return txt
|
|
else:
|
|
# On suppose d'abord que le texte est en UTF-8
|
|
try:
|
|
return txt.decode("UTF-8")
|
|
except:
|
|
# Sinon c'est surement de l'iso
|
|
# donc on le décode en utf-8 \o/
|
|
return txt.decode("UTF-8")
|
|
|
|
def to_encoding(txt, enc=ENCODING):
|
|
return to_unicode(txt).encode(enc, 'ignore')
|
|
|
|
def cprint(txt, col='blanc', newline=True):
|
|
t = coul(to_encoding(txt), col)
|
|
if newline:
|
|
print t
|
|
else:
|
|
print t,
|
|
|
|
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
|
|
"""
|
|
sep_col = u'|'
|
|
if data:
|
|
nbcols = len(data[0])
|
|
elif titre:
|
|
nbcols = len(titre)
|
|
else:
|
|
return u'Aucune donnée'
|
|
|
|
# Formats
|
|
#########
|
|
if not format:
|
|
format = ['s'] * nbcols
|
|
|
|
def reformate(data, format):
|
|
if format == 's':
|
|
try:
|
|
return unicode(data)
|
|
except:
|
|
sys.stderr.write("Cannot cast to unicode %r\n" % data)
|
|
return unicode(data, errors='ignore')
|
|
|
|
elif format == 'o':
|
|
data = float(data)
|
|
if data > 1024**3:
|
|
return str(round(data/1024**3, 1)) + 'Go'
|
|
elif data > 1024**2:
|
|
return str(round(data/1024**2, 1)) + 'Mo'
|
|
elif data > 1024:
|
|
return str(round(data/1024, 1)) + 'ko'
|
|
else:
|
|
return str(round(data, 1)) + 'o'
|
|
|
|
data = [[reformate(ligne[i], format[i]) for i in range(nbcols)] for ligne in data]
|
|
|
|
# Largeurs
|
|
##########
|
|
if not largeur:
|
|
largeur = [max([len(re.sub('\\\Z.' if dialog else '\x1b\[1;([0-9]|[0-9][0-9])m', '', ligne[i])) for ligne in data]) for i in range(nbcols)]
|
|
elif '*' in largeur:
|
|
if width:
|
|
cols = width
|
|
else:
|
|
rows, cols = get_screen_size()
|
|
if dialog:
|
|
cols = cols - 6
|
|
for i in range(nbcols):
|
|
if largeur[i] in ['*', -1]:
|
|
largeur[i] = max(cols - sum([l for l in largeur if l != '*']) - nbcols - 1, 3)
|
|
break
|
|
|
|
# Alignement
|
|
############
|
|
if not alignement:
|
|
alignement = ['c'] * nbcols
|
|
|
|
def aligne(data, alignement, largeur):
|
|
# Longeur sans les chaines de formatage
|
|
l = len(re.sub('\\\Z.' if dialog else '\x1b\[1;([0-9]|[0-9][0-9])m', '', data))
|
|
|
|
# Alignement
|
|
if l > largeur:
|
|
# découpage d'une chaine trop longue
|
|
regexp = re.compile('\\\Z.' if dialog else '\x1b\[1;([0-9]|[0-9][0-9])m')
|
|
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 and not s.start():
|
|
# c'est de la mise en forme
|
|
new_data += data[:s.end()]
|
|
data = data[s.end():]
|
|
elif new_len < largeur - 1:
|
|
# c'est un caratère normal, et il y a la place
|
|
new_data += data[0]
|
|
data = data[1:]
|
|
new_len += 1
|
|
else:
|
|
# c'est un caratère normal mais on a dépassé le max
|
|
data = data[1:]
|
|
if not data:
|
|
return new_data + '*'
|
|
|
|
elif l == largeur:
|
|
return data
|
|
|
|
elif alignement == 'g':
|
|
return u' ' + data + u' '*(largeur-l-1)
|
|
|
|
elif alignement == 'd':
|
|
return u' '*(largeur-l-1) + data + u' '
|
|
|
|
else:
|
|
return u' '*((largeur-l)/2) + data + u' '*((largeur-l+1)/2)
|
|
|
|
data = [[aligne(ligne[i], alignement[i], largeur[i]) for i in range(nbcols)] for ligne in data]
|
|
|
|
# Le titre
|
|
##########
|
|
if titre:
|
|
# ligne de titre
|
|
chaine = sep_col + sep_col.join([aligne(titre[i], 'c', largeur[i]) for i in range(nbcols)]) + sep_col + u'\n'
|
|
# ligne de séparation
|
|
chaine += sep_col + u'+'.join([u'-'*largeur[i] for i in range(nbcols)]) + 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(nbcols)]) + sep_col for ligne in data])
|
|
|
|
return chaine
|
|
|
|
def get_screen_size():
|
|
"""Retourne la taille de l'écran.
|
|
|
|
Sous la forme d'un tuble (lignes, colonnes)"""
|
|
|
|
try:
|
|
from termios import TIOCGWINSZ
|
|
from struct import pack, unpack
|
|
from fcntl import ioctl
|
|
s = pack("HHHH", 0, 0, 0, 0)
|
|
rows, cols = unpack("HHHH", ioctl(sys.stdout.fileno(), TIOCGWINSZ, s))[:2]
|
|
return (rows, cols)
|
|
except:
|
|
return (24, 80)
|
|
|
|
def prompt(prompt, defaut=u'', couleur='gras'):
|
|
"""
|
|
Pose la question prompt en couleur (défaut gras), retourne la réponse.
|
|
"""
|
|
prompt = cranslib.cransstrings.decode_dammit(prompt)
|
|
defaut = cranslib.cransstrings.decode_dammit(defaut)
|
|
# coul renvoie alors un unicode
|
|
prompt_s = coul(prompt, couleur)
|
|
if defaut:
|
|
prompt_s += u" [%s]" % defaut
|
|
# On fait tout pour ne pas faire crasher le script appelant
|
|
# si on lève une erreur, on la rattrappe et on laisse une chance
|
|
# de refaire une proposition.
|
|
# On laisse néanmoins la possibilité de sortir sur un Ctrl+C
|
|
while True:
|
|
try:
|
|
v = raw_input(prompt_s.encode(ENCODING))
|
|
v = cranslib.cransstrings.decode_dammit(v).strip()
|
|
if not v:
|
|
v = defaut
|
|
return v
|
|
except UnicodeDecodeError as error:
|
|
try:
|
|
print "UnicodeDecodeError rattrappée (chaîne : %s)" % v
|
|
except UnicodeDecodeError as error2:
|
|
print "UnicodeDecodeError catched but could not be displayed. Trying ASCII."
|
|
|
|
class anim:
|
|
""" Permet de créer une animation :
|
|
truc................./
|
|
truc.................-
|
|
truc.................\
|
|
truc.................|
|
|
ou une barre de progression si le nombre total d'itérations est founi.
|
|
"""
|
|
def __init__(self, truc, iter=0):
|
|
""" Affichage de :
|
|
truc................."""
|
|
self.txt = truc + '.'*(45-len(truc))
|
|
self.c = 1
|
|
self.iter = iter
|
|
sys.stdout.write(self.txt)
|
|
sys.stdout.flush()
|
|
|
|
def reinit(self):
|
|
""" Efface la ligne courrante et
|
|
affiche : truc................. """
|
|
sys.stdout.write(el + self.txt)
|
|
if self.iter:
|
|
sys.stdout.write(' '*28)
|
|
sys.stdout.write(el + self.txt)
|
|
sys.stdout.flush()
|
|
|
|
def cycle(self):
|
|
""" Efface la ligne courrante et
|
|
affiche : truc..................?
|
|
? caratère variant à chaque appel """
|
|
if self.iter != 0:
|
|
prog = float(self.c) / float(self.iter)
|
|
pprog = float(self.c-1) / float(self.iter)
|
|
n = int(20 * prog)
|
|
if 100*prog != 100*pprog:
|
|
msg = "%s[%s>%s] %3i%%" % (self.txt, '='*n, ' '*(20 - n), int(100*prog))
|
|
sys.stdout.write(el + msg)
|
|
else:
|
|
sys.stdout.write(el + self.txt)
|
|
sys.stdout.write('/-\|'[self.c%4])
|
|
sys.stdout.flush()
|
|
self.c += 1
|