|
| 1 | +""" |
| 2 | +Module pour les intéractions avec la ligne de commande. |
| 3 | +""" |
| 4 | + |
| 5 | +import sys |
| 6 | +from pathlib import Path |
| 7 | +from json import load |
| 8 | +from time import sleep |
| 9 | +from random import choice |
| 10 | +import colorama |
| 11 | +import threading |
| 12 | + |
| 13 | +class AfficheurCLI: |
| 14 | + """ |
| 15 | + Représente une classe pour afficher des informations dans la ligne de commande. |
| 16 | +
|
| 17 | + Attributes: |
| 18 | + _thread_chargement (Union[None,Thread]): Le thread de l'animation de chargement. |
| 19 | + _thread_chargement_termine (Event): L'évènement pour demander au thread de |
| 20 | + l'animation de chargement de s'arrêter lorsque le chargement est terminé. |
| 21 | + _thread_chargement_erreur (Event): L'évènement pour demander au thread de |
| 22 | + l'animation de chargement de s'arrêter lorsque une erreur s'est produite. |
| 23 | + _animations_actuelles (dict): Les éléments visuels pour l'animation de chargement. |
| 24 | +
|
| 25 | + Class-level variables: |
| 26 | + :cvar COULEUR_MESSAGE_NORMAL (str): La couleur pour les messages normaux en CLI. |
| 27 | + :cvar COULEUR_MESSAGE_ERREUR (str): La couleur pour les messages d'erreur en CLI. |
| 28 | + """ |
| 29 | + COULEUR_MESSAGE_NORMAL = colorama.Fore.WHITE |
| 30 | + COULEUR_MESSAGE_ERREUR = colorama.Fore.RED |
| 31 | + |
| 32 | + def __init__(self): |
| 33 | + """ |
| 34 | + Initialise un objet pour afficher des informations dans la ligne de commande. |
| 35 | + """ |
| 36 | + # Normalise les codes couleurs pour fonctionner partout |
| 37 | + colorama.init() |
| 38 | + # Initialise les variables pour le chargement |
| 39 | + self._thread_chargement = None |
| 40 | + self._thread_chargement_termine = threading.Event() |
| 41 | + self._thread_chargement_erreur = threading.Event() |
| 42 | + # Récupère les animations |
| 43 | + chemin_racine = Path(__file__).parent.parent.parent.resolve() |
| 44 | + chemin_animations = chemin_racine / "assets" / "animations.json" |
| 45 | + with open(chemin_animations, "r", encoding="utf-8") as animations: |
| 46 | + elements_animations = load(animations) |
| 47 | + # Choisis une animation au hasard parmi chaque catégorie d'animation |
| 48 | + self._animations_actuelles = { |
| 49 | + "chasseur": choice(elements_animations["chasseurs"]), |
| 50 | + "fantome": choice(elements_animations["fantomes"]), |
| 51 | + "rayon_laser": elements_animations["rayons_laser"] |
| 52 | + } |
| 53 | + |
| 54 | + def reecrire_ligne(self, message: str): |
| 55 | + """ |
| 56 | + Permet d'écrire des caractères par dessus la dernière ligne dans la |
| 57 | + ligne de commande. |
| 58 | +
|
| 59 | + Args: |
| 60 | + message (str): Les caractères à afficher. |
| 61 | +
|
| 62 | + Returns: |
| 63 | + None |
| 64 | +
|
| 65 | + Raises: |
| 66 | + TypeError: Le paramètre ``message`` n'est pas une chaîne de caractères. |
| 67 | + """ |
| 68 | + # Validation du paramètre |
| 69 | + if not isinstance(message, str): |
| 70 | + raise TypeError("Le message pour la réécriture doit être une chaîne de caractères.") |
| 71 | + # Ecriture du message |
| 72 | + sys.stdout.write("\r" + self.COULEUR_MESSAGE_NORMAL + message) |
| 73 | + sys.stdout.flush() |
| 74 | + |
| 75 | + def affiche_message(self, message: str): |
| 76 | + """ |
| 77 | + Permet d'écrire un message commun dans la ligne de commande avec la bonne |
| 78 | + couleur. |
| 79 | +
|
| 80 | + Args: |
| 81 | + message (str): Le message à afficher. |
| 82 | +
|
| 83 | + Returns: |
| 84 | + None |
| 85 | +
|
| 86 | + Raises: |
| 87 | + TypeError: Le paramètre ``message`` n'est pas une chaîne de caractères. |
| 88 | + """ |
| 89 | + # Validation du paramètre |
| 90 | + if not isinstance(message, str): |
| 91 | + raise TypeError("Le message doit être une chaîne de caractères.") |
| 92 | + # Ecriture du message |
| 93 | + print(self.COULEUR_MESSAGE_NORMAL + message, flush=True) |
| 94 | + |
| 95 | + def affiche_erreur(self, message: str, exception: Exception): |
| 96 | + """ |
| 97 | + Permet d'écrire un message d'erreur dans la ligne de commande avec la bonne |
| 98 | + couleur. |
| 99 | +
|
| 100 | + Args: |
| 101 | + message (str): Le message à afficher. |
| 102 | + exception (Exception): L'exception à afficher. |
| 103 | +
|
| 104 | + Returns: |
| 105 | + None |
| 106 | +
|
| 107 | + Raises: |
| 108 | + TypeError: Le paramètre ``message`` n'est pas une chaîne de caractères ou |
| 109 | + le paramètre ``exception`` n'est pas une instance de la classe :class:`Exception`. |
| 110 | + """ |
| 111 | + # Validation des paramètres |
| 112 | + if not isinstance(message, str): |
| 113 | + raise TypeError("Le message d'erreur doit être une chaîne de caractères.") |
| 114 | + if not isinstance(exception, Exception): |
| 115 | + raise TypeError("L'exception à afficher doit être une instance de Exception.") |
| 116 | + # Ecriture du message |
| 117 | + print(self.COULEUR_MESSAGE_ERREUR + f"{message}\n{exception}", flush=True) |
| 118 | + |
| 119 | + def lance_animation_chargement(self): |
| 120 | + """ |
| 121 | + Lance une animation de chargement dans la ligne de commande via un thread non bloquant. |
| 122 | + Si l'animation de chargement est déjà en cours, cette méthode ne fait rien. |
| 123 | +
|
| 124 | + Returns: |
| 125 | + None |
| 126 | + """ |
| 127 | + # Si le thread est déjà lancé, annulation de l'animation |
| 128 | + if self._thread_chargement is None: |
| 129 | + # On réinitialise les demandes d'arrêt |
| 130 | + self._thread_chargement_termine.clear() |
| 131 | + self._thread_chargement_erreur.clear() |
| 132 | + # Initialisation du thread pour le chargement |
| 133 | + self._thread_chargement = threading.Thread(target=self._animation_chargement, daemon=True) |
| 134 | + # Lancement du thread pour le chargement |
| 135 | + self._thread_chargement.start() |
| 136 | + |
| 137 | + def _animation_chargement(self): |
| 138 | + """ |
| 139 | + Lance l'animation de chargement en boucle jusqu'à la demande d'arrêt via |
| 140 | + l'attribut :attr:`_thread_chargement_demande_arret`. |
| 141 | +
|
| 142 | + Returns: |
| 143 | + None |
| 144 | + """ |
| 145 | + # Eléments de l'animation de chargement |
| 146 | + chasseur_chargement = self._animations_actuelles["chasseur"][0] |
| 147 | + fantome_chargement = self._animations_actuelles["fantome"][0] |
| 148 | + signes_rayon_laser = self._animations_actuelles["rayon_laser"] |
| 149 | + couleurs = ["\033[91m", "\033[93m", "\033[94m", "\033[95m"] # Rouge, Jaune, Bleu, Magenta |
| 150 | + |
| 151 | + # Eléments de l'animation de fin de chargement en cas de succès |
| 152 | + chasseur_gagne = self._animations_actuelles["chasseur"][2] |
| 153 | + fantome_perd = self._animations_actuelles["fantome"][1] |
| 154 | + |
| 155 | + # Eléments de l'animation de fin de chargement en cas d'erreur |
| 156 | + chasseur_perd = self._animations_actuelles["chasseur"][1] |
| 157 | + fantome_gagne = self._animations_actuelles["fantome"][2] |
| 158 | + |
| 159 | + # Variables pour l'animation de chargement |
| 160 | + index_boucle = 0 |
| 161 | + rayon_laser = "" |
| 162 | + |
| 163 | + # Début de l'animation (jusqu'à la demande d'arrêt) |
| 164 | + while not (self._thread_chargement_termine.is_set() |
| 165 | + or self._thread_chargement_erreur.is_set()): |
| 166 | + # Arrête d'ajouter des caractères lorsque la chaîne est trop longue |
| 167 | + if (index_boucle < 40): |
| 168 | + # Récupération de la prochaine couleur |
| 169 | + couleur_courante = couleurs[(index_boucle % len(couleurs))] |
| 170 | + # Récupération du prochain signe du rayon |
| 171 | + signe_courant = signes_rayon_laser[(index_boucle % len(signes_rayon_laser))] |
| 172 | + # Ajout du dernier signe avec la nouvelle couleur au rayon |
| 173 | + rayon_laser += couleur_courante + signe_courant |
| 174 | + # Réactualisation de l'animation de chargement |
| 175 | + self.reecrire_ligne(f"{chasseur_chargement}{rayon_laser}\033[0m{fantome_chargement}") |
| 176 | + index_boucle += 1 |
| 177 | + sleep(0.05) |
| 178 | + |
| 179 | + # Suppression de la ligne de chargement |
| 180 | + self.reecrire_ligne("\033[K") |
| 181 | + espace_rayon_laser = " " * index_boucle |
| 182 | + if (self._thread_chargement_termine.is_set()): |
| 183 | + # Message d'animation terminée |
| 184 | + self.reecrire_ligne(f"{chasseur_gagne}{espace_rayon_laser}\033[0m{fantome_perd}\n") |
| 185 | + self.affiche_message(f"Analyse terminée! We came, we saw, we logged it.") |
| 186 | + else: |
| 187 | + # Message d'animation erreur |
| 188 | + self.reecrire_ligne(f"{chasseur_perd}{espace_rayon_laser}\033[0m{fantome_gagne}\n") |
| 189 | + |
| 190 | + def stop_animation_chargement(self, erreur: bool = False): |
| 191 | + """ |
| 192 | + Lance une demande d'arrêt au thread qui gère l'animation de chargement |
| 193 | + en cours. Si aucune animation n'est en cours, cette méthode ne fait rien. |
| 194 | +
|
| 195 | + Args: |
| 196 | + erreur (bool): Indique si la demande d'arrêt est dûe à une erreur ou non. |
| 197 | +
|
| 198 | + Returns: |
| 199 | + None |
| 200 | + """ |
| 201 | + # Si le thread de chargement existe et est lancé |
| 202 | + if self._thread_chargement and self._thread_chargement.is_alive(): |
| 203 | + # Lancement de la demade d'arrêt |
| 204 | + if not erreur: |
| 205 | + self._thread_chargement_termine.set() |
| 206 | + else: |
| 207 | + self._thread_chargement_erreur.set() |
| 208 | + # Attente de l'arrêt depuis le thread principal |
| 209 | + self._thread_chargement.join() |
| 210 | + self._thread_chargement = None |
0 commit comments