clarity.core
1# coding: utf-8 2import os 3import sys 4import shutil 5import time 6import json 7import subprocess 8from typing import List, Tuple, Callable, Optional 9 10# --- Configuration Globale ---------------------------------------------------- 11class AppConfig: 12 """Stocke la configuration globale de l'application.""" 13 HOME_PATH: Optional[str] = None 14 15# --- Couleurs cross-platform (Colorama sur Windows) -------------------------- 16try: 17 import colorama 18 colorama.init(autoreset=True) 19except ImportError: 20 pass # Optionnel 21 22class Colors: 23 RED = "\033[91m" 24 GREEN = "\033[92m" 25 YELLOW = "\033[93m" 26 CYAN = "\033[96m" 27 MAGENTA = "\033[95m" 28 RESET = "\033[0m" 29 BOLD = "\033[1m" 30 BLUE = "\033[94m" 31 ORANGE = "\033[38;5;208m" 32 DIM = "\033[2m" 33 GRAY = "\033[90m" 34 35def clear(): 36 os.system('cls' if os.name == 'nt' else 'clear') 37 38def hr(char="─", width=60, color=Colors.GRAY): 39 return f"{color}{char * width}{Colors.RESET}" 40 41def badge(text: str, color=Colors.CYAN): 42 return f"{color}[{text}]{Colors.RESET}" 43 44def soft_input(prompt: str) -> str: 45 try: 46 return input(prompt) 47 except (EOFError, KeyboardInterrupt): 48 print() # Nouvelle ligne après Ctrl+C 49 return "" 50 51def load_version(default="v?.?") -> str: 52 """Charge la version depuis version.txt de manière fiable.""" 53 try: 54 # __file__ est le chemin du fichier actuel (core.py) 55 current_dir = os.path.dirname(os.path.abspath(__file__)) 56 # Le fichier version.txt est dans le dossier parent (rework) 57 version_file = os.path.join(current_dir, "..", "..", "version.txt") 58 if not os.path.exists(version_file): 59 # Fallback si on est dans le dossier principal du projet 60 version_file = os.path.join(current_dir, "..", "version.txt") 61 62 with open(version_file, "r", encoding="utf-8") as f: 63 return f.readline().strip() 64 except (FileNotFoundError, IOError): 65 return default 66 67VERSION = load_version() 68 69def menu(): 70 # header visuel moderne 71 print(f""" 72{Colors.CYAN}{Colors.BOLD} 73 ____ _ _ _ ____ _ ___ 74 / ___| | __ _ _ __(_) |_ _ _ / ___| | |_ _| 75 | | | |/ _` | '__| | __| | | | | | | | | 76 | |___| | (_| | | | | |_| |_| | |___| |___ | | 77 \____|_|\__,_|_| |_|\__|\|, |\____|_____|___| 78 |___/ 79{Colors.RESET}{badge('CLI')} {badge(VERSION, Colors.ORANGE)} {badge('Modern UI', Colors.MAGENTA)} 80{hr()} 81{Colors.DIM}Astuce: tape un numéro, 's' pour rechercher, 'q' pour quitter.{Colors.RESET} 82""") 83 84# --- Helpers système --------------------------------------------------------- 85def run_cmd(cmd: str, cwd: Optional[str] = None) -> int: 86 """Exécute une commande shell de manière robuste dans un répertoire donné (cwd).""" 87 try: 88 result = subprocess.run(cmd, shell=True, check=False, capture_output=True, text=True, cwd=cwd) 89 if result.stdout: 90 print(result.stdout.strip()) 91 if result.stderr: 92 print(f"{Colors.RED}{result.stderr.strip()}{Colors.RESET}", file=sys.stderr) 93 return result.returncode 94 except Exception as e: 95 print(f"{Colors.RED}Erreur critique en exécutant la commande: {e}{Colors.RESET}", file=sys.stderr) 96 return -1 97 98def rm_rf(path: str): 99 if not os.path.exists(path): 100 return 101 try: 102 if os.path.isfile(path) or os.path.islink(path): 103 os.remove(path) 104 else: 105 shutil.rmtree(path) 106 except (IOError, OSError) as e: 107 print(f"{Colors.RED}Impossible de supprimer {path}: {e}{Colors.RESET}", file=sys.stderr) 108 109 110def ensure_dir(path: str): 111 try: 112 os.makedirs(path, exist_ok=True) 113 except OSError as e: 114 print(f"{Colors.RED}Impossible de créer le dossier {path}: {e}{Colors.RESET}", file=sys.stderr) 115 raise # Renvoyer l'exception peut être nécessaire pour arrêter le programme 116 117def expand_path(p: str) -> str: 118 return os.path.normpath(os.path.abspath(os.path.expanduser(p))) 119 120# --- Base classes ------------------------------------------------------------ 121class ClarityTool(object): 122 TITLE: str = "" 123 DESCRIPTION: str = "" 124 INSTALL_COMMANDS: List[str] = [] 125 UNINSTALL_COMMANDS: List[str] = [] 126 RUN_COMMANDS: List[str] = [] 127 OPTIONS: List[Tuple[str, Callable]] = [] 128 PROJECT_URL: str = "" 129 130 def __init__(self, options=None, installable: bool = True, runnable: bool = True): 131 options = options or [] 132 self.OPTIONS = [] 133 if installable: 134 self.OPTIONS.append(('Install', self.install)) 135 if runnable: 136 self.OPTIONS.append(('Run', self.run)) 137 if isinstance(options, list): 138 self.OPTIONS.extend(options) 139 else: 140 # Utilisons une exception plus spécifique 141 raise TypeError("options must be a list of (option_name, option_fn) tuples") 142 143 def _print_card(self): 144 clear() 145 menu() 146 print(f"{Colors.BOLD}{self.TITLE}{Colors.RESET} {badge('TOOL', Colors.GREEN)}") 147 if self.DESCRIPTION: 148 print(f"{Colors.GRAY}{self.DESCRIPTION}{Colors.RESET}") 149 if self.PROJECT_URL: 150 print(f"{Colors.BLUE}↪ {self.PROJECT_URL}{Colors.RESET}") 151 print(hr()) 152 153 def show_info(self): 154 self._print_card() 155 156 def _print_options(self, parent=None): 157 for index, option in enumerate(self.OPTIONS): 158 print(f"{Colors.CYAN}[{index + 1}]{Colors.RESET} {option[0]}") 159 if self.PROJECT_URL: 160 print(f"{Colors.CYAN}[98]{Colors.RESET} Ouvrir la page du projet") 161 print(f"{Colors.YELLOW}[99]{Colors.RESET} Retour vers {parent.TITLE if parent else 'Quitter'}") 162 print(hr()) 163 164 def show_options(self, parent=None): 165 while True: 166 self.show_info() 167 self._print_options(parent) 168 option_index = soft_input(f"{badge('Select')} ").strip().lower() 169 170 if not option_index: # L'utilisateur a pressé Ctrl+C 171 return 99 172 173 if option_index == 'q': 174 sys.exit(0) 175 176 if option_index == 's': 177 print(f"{Colors.DIM}Recherche non disponible dans ce sous-menu.{Colors.RESET}") 178 time.sleep(1) 179 continue 180 181 try: 182 option_index = int(option_index) 183 if option_index - 1 in range(len(self.OPTIONS)): 184 # Appelle la fonction de l'option (ex: self.install) 185 ret_code = self.OPTIONS[option_index - 1][1]() 186 if ret_code != 99: 187 soft_input("\n↵ ENTER pour continuer…") 188 elif option_index == 98 and self.PROJECT_URL: 189 import webbrowser 190 webbrowser.open_new_tab(self.PROJECT_URL) 191 elif option_index == 99: 192 return 99 193 except (TypeError, ValueError): 194 print(f"{Colors.RED}Entrée invalide.{Colors.RESET}") 195 time.sleep(0.8) 196 except Exception as e: 197 print(f"{Colors.RED}Erreur inattendue: {e}{Colors.RESET}") 198 soft_input("\n↵ ENTER pour continuer…") 199 200 def before_install(self): pass 201 def install(self): 202 self.before_install() 203 if not AppConfig.HOME_PATH: 204 print(f"{Colors.RED}Le chemin d'installation (HOME_PATH) n'est pas configuré.{Colors.RESET}") 205 return 1 206 207 print(f"Installation des outils dans : {AppConfig.HOME_PATH}") 208 for cmd in self.INSTALL_COMMANDS: 209 code = run_cmd(cmd, cwd=AppConfig.HOME_PATH) 210 if code != 0: 211 print(f"{Colors.RED}Échec de la commande: {cmd} (code: {code}){Colors.RESET}") 212 return code 213 self.after_install() 214 return 0 215 def after_install(self): print(f"{Colors.GREEN}Installation terminée avec succès!{Colors.RESET}") 216 217 def before_uninstall(self) -> bool: return True 218 def uninstall(self): 219 if self.before_uninstall(): 220 for cmd in self.UNINSTALL_COMMANDS: 221 run_cmd(cmd, cwd=AppConfig.HOME_PATH) 222 self.after_uninstall() 223 def after_uninstall(self): pass 224 225 def before_run(self): pass 226 def run(self): 227 self.before_run() 228 if not AppConfig.HOME_PATH: 229 print(f"{Colors.RED}Le chemin d'installation (HOME_PATH) n'est pas configuré.{Colors.RESET}") 230 return 1 231 232 for cmd in self.RUN_COMMANDS: 233 run_cmd(cmd, cwd=AppConfig.HOME_PATH) 234 self.after_run() 235 return 0 236 def after_run(self): pass 237 238class ClarityToolsCollection(ClarityTool): 239 TOOLS: List[object] = [] 240 241 def show_options(self, parent=None): 242 page = 0 243 per_page = 10 244 245 def filtered_tools(query: Optional[str]): 246 if not query: 247 return self.TOOLS 248 q = query.lower().strip() 249 return [t for t in self.TOOLS if q in getattr(t, "TITLE", "").lower() or q in getattr(t, "DESCRIPTION", "").lower()] 250 251 current_filter = None 252 while True: 253 clear() 254 menu() 255 title_line = f"{Colors.BOLD}{self.TITLE}{Colors.RESET} {badge('COLLECTION', Colors.GREEN)}" 256 if current_filter: 257 title_line += f" {badge('filter:'+current_filter, Colors.ORANGE)}" 258 print(title_line) 259 print(hr()) 260 261 tools = filtered_tools(current_filter) 262 if not tools: 263 print(f"{Colors.YELLOW}Aucun outil ne correspond à votre recherche.{Colors.RESET}") 264 else: 265 start = page * per_page 266 end = min(start + per_page, len(tools)) 267 for idx, tool in enumerate(tools[start:end], start=1): 268 print(f"{Colors.CYAN}[{idx}]{Colors.RESET} {tool.TITLE} {Colors.DIM}- {tool.DESCRIPTION or ''}{Colors.RESET}") 269 270 total_pages = max(1, (len(tools) + per_page - 1) // per_page) 271 print(hr()) 272 print(f"{Colors.GRAY}Page {page+1}/{total_pages} | 'n' suivant • 'p' précédent • 's' recherche • 'q' quitter{Colors.RESET}") 273 274 print(f"{Colors.YELLOW}[99]{Colors.RESET} Retour vers {parent.TITLE if parent else 'Quitter'}") 275 choice = soft_input(f"{badge('Choose')} ").strip().lower() 276 277 if not choice: # Ctrl+C 278 return 99 279 if choice == 'q': sys.exit(0) 280 if choice == 'n': 281 if tools and (page + 1) * per_page < len(tools): page += 1 282 continue 283 if choice == 'p': 284 if page > 0: page -= 1 285 continue 286 if choice == 's': 287 current_filter = soft_input("Rechercher: ").strip() 288 page = 0 289 continue 290 291 try: 292 if choice == '99': 293 return 99 294 295 choice_int = int(choice) 296 start = page * per_page 297 index_global = start + (choice_int - 1) 298 if 0 <= index_global < len(tools): 299 tools[index_global].show_options(parent=self) 300 else: 301 print(f"{Colors.RED}Choix invalide.{Colors.RESET}") 302 time.sleep(0.8) 303 except (TypeError, ValueError): 304 print(f"{Colors.RED}Entrée invalide.{Colors.RESET}") 305 time.sleep(0.8) 306 except Exception as e: 307 print(f"{Colors.RED}Erreur inattendue: {e}{Colors.RESET}") 308 soft_input("\n↵ ENTER pour continuer…")
class
AppConfig:
12class AppConfig: 13 """Stocke la configuration globale de l'application.""" 14 HOME_PATH: Optional[str] = None
Stocke la configuration globale de l'application.
class
Colors:
def
clear():
def
hr(char='─', width=60, color='\x1b[90m'):
def
badge(text: str, color='\x1b[96m'):
def
soft_input(prompt: str) -> str:
def
load_version(default='v?.?') -> str:
52def load_version(default="v?.?") -> str: 53 """Charge la version depuis version.txt de manière fiable.""" 54 try: 55 # __file__ est le chemin du fichier actuel (core.py) 56 current_dir = os.path.dirname(os.path.abspath(__file__)) 57 # Le fichier version.txt est dans le dossier parent (rework) 58 version_file = os.path.join(current_dir, "..", "..", "version.txt") 59 if not os.path.exists(version_file): 60 # Fallback si on est dans le dossier principal du projet 61 version_file = os.path.join(current_dir, "..", "version.txt") 62 63 with open(version_file, "r", encoding="utf-8") as f: 64 return f.readline().strip() 65 except (FileNotFoundError, IOError): 66 return default
Charge la version depuis version.txt de manière fiable.
VERSION =
'1.1.0'
def
run_cmd(cmd: str, cwd: Optional[str] = None) -> int:
86def run_cmd(cmd: str, cwd: Optional[str] = None) -> int: 87 """Exécute une commande shell de manière robuste dans un répertoire donné (cwd).""" 88 try: 89 result = subprocess.run(cmd, shell=True, check=False, capture_output=True, text=True, cwd=cwd) 90 if result.stdout: 91 print(result.stdout.strip()) 92 if result.stderr: 93 print(f"{Colors.RED}{result.stderr.strip()}{Colors.RESET}", file=sys.stderr) 94 return result.returncode 95 except Exception as e: 96 print(f"{Colors.RED}Erreur critique en exécutant la commande: {e}{Colors.RESET}", file=sys.stderr) 97 return -1
Exécute une commande shell de manière robuste dans un répertoire donné (cwd).
def
rm_rf(path: str):
99def rm_rf(path: str): 100 if not os.path.exists(path): 101 return 102 try: 103 if os.path.isfile(path) or os.path.islink(path): 104 os.remove(path) 105 else: 106 shutil.rmtree(path) 107 except (IOError, OSError) as e: 108 print(f"{Colors.RED}Impossible de supprimer {path}: {e}{Colors.RESET}", file=sys.stderr)
def
ensure_dir(path: str):
def
expand_path(p: str) -> str:
class
ClarityTool:
122class ClarityTool(object): 123 TITLE: str = "" 124 DESCRIPTION: str = "" 125 INSTALL_COMMANDS: List[str] = [] 126 UNINSTALL_COMMANDS: List[str] = [] 127 RUN_COMMANDS: List[str] = [] 128 OPTIONS: List[Tuple[str, Callable]] = [] 129 PROJECT_URL: str = "" 130 131 def __init__(self, options=None, installable: bool = True, runnable: bool = True): 132 options = options or [] 133 self.OPTIONS = [] 134 if installable: 135 self.OPTIONS.append(('Install', self.install)) 136 if runnable: 137 self.OPTIONS.append(('Run', self.run)) 138 if isinstance(options, list): 139 self.OPTIONS.extend(options) 140 else: 141 # Utilisons une exception plus spécifique 142 raise TypeError("options must be a list of (option_name, option_fn) tuples") 143 144 def _print_card(self): 145 clear() 146 menu() 147 print(f"{Colors.BOLD}{self.TITLE}{Colors.RESET} {badge('TOOL', Colors.GREEN)}") 148 if self.DESCRIPTION: 149 print(f"{Colors.GRAY}{self.DESCRIPTION}{Colors.RESET}") 150 if self.PROJECT_URL: 151 print(f"{Colors.BLUE}↪ {self.PROJECT_URL}{Colors.RESET}") 152 print(hr()) 153 154 def show_info(self): 155 self._print_card() 156 157 def _print_options(self, parent=None): 158 for index, option in enumerate(self.OPTIONS): 159 print(f"{Colors.CYAN}[{index + 1}]{Colors.RESET} {option[0]}") 160 if self.PROJECT_URL: 161 print(f"{Colors.CYAN}[98]{Colors.RESET} Ouvrir la page du projet") 162 print(f"{Colors.YELLOW}[99]{Colors.RESET} Retour vers {parent.TITLE if parent else 'Quitter'}") 163 print(hr()) 164 165 def show_options(self, parent=None): 166 while True: 167 self.show_info() 168 self._print_options(parent) 169 option_index = soft_input(f"{badge('Select')} ").strip().lower() 170 171 if not option_index: # L'utilisateur a pressé Ctrl+C 172 return 99 173 174 if option_index == 'q': 175 sys.exit(0) 176 177 if option_index == 's': 178 print(f"{Colors.DIM}Recherche non disponible dans ce sous-menu.{Colors.RESET}") 179 time.sleep(1) 180 continue 181 182 try: 183 option_index = int(option_index) 184 if option_index - 1 in range(len(self.OPTIONS)): 185 # Appelle la fonction de l'option (ex: self.install) 186 ret_code = self.OPTIONS[option_index - 1][1]() 187 if ret_code != 99: 188 soft_input("\n↵ ENTER pour continuer…") 189 elif option_index == 98 and self.PROJECT_URL: 190 import webbrowser 191 webbrowser.open_new_tab(self.PROJECT_URL) 192 elif option_index == 99: 193 return 99 194 except (TypeError, ValueError): 195 print(f"{Colors.RED}Entrée invalide.{Colors.RESET}") 196 time.sleep(0.8) 197 except Exception as e: 198 print(f"{Colors.RED}Erreur inattendue: {e}{Colors.RESET}") 199 soft_input("\n↵ ENTER pour continuer…") 200 201 def before_install(self): pass 202 def install(self): 203 self.before_install() 204 if not AppConfig.HOME_PATH: 205 print(f"{Colors.RED}Le chemin d'installation (HOME_PATH) n'est pas configuré.{Colors.RESET}") 206 return 1 207 208 print(f"Installation des outils dans : {AppConfig.HOME_PATH}") 209 for cmd in self.INSTALL_COMMANDS: 210 code = run_cmd(cmd, cwd=AppConfig.HOME_PATH) 211 if code != 0: 212 print(f"{Colors.RED}Échec de la commande: {cmd} (code: {code}){Colors.RESET}") 213 return code 214 self.after_install() 215 return 0 216 def after_install(self): print(f"{Colors.GREEN}Installation terminée avec succès!{Colors.RESET}") 217 218 def before_uninstall(self) -> bool: return True 219 def uninstall(self): 220 if self.before_uninstall(): 221 for cmd in self.UNINSTALL_COMMANDS: 222 run_cmd(cmd, cwd=AppConfig.HOME_PATH) 223 self.after_uninstall() 224 def after_uninstall(self): pass 225 226 def before_run(self): pass 227 def run(self): 228 self.before_run() 229 if not AppConfig.HOME_PATH: 230 print(f"{Colors.RED}Le chemin d'installation (HOME_PATH) n'est pas configuré.{Colors.RESET}") 231 return 1 232 233 for cmd in self.RUN_COMMANDS: 234 run_cmd(cmd, cwd=AppConfig.HOME_PATH) 235 self.after_run() 236 return 0 237 def after_run(self): pass
ClarityTool(options=None, installable: bool = True, runnable: bool = True)
131 def __init__(self, options=None, installable: bool = True, runnable: bool = True): 132 options = options or [] 133 self.OPTIONS = [] 134 if installable: 135 self.OPTIONS.append(('Install', self.install)) 136 if runnable: 137 self.OPTIONS.append(('Run', self.run)) 138 if isinstance(options, list): 139 self.OPTIONS.extend(options) 140 else: 141 # Utilisons une exception plus spécifique 142 raise TypeError("options must be a list of (option_name, option_fn) tuples")
def
show_options(self, parent=None):
165 def show_options(self, parent=None): 166 while True: 167 self.show_info() 168 self._print_options(parent) 169 option_index = soft_input(f"{badge('Select')} ").strip().lower() 170 171 if not option_index: # L'utilisateur a pressé Ctrl+C 172 return 99 173 174 if option_index == 'q': 175 sys.exit(0) 176 177 if option_index == 's': 178 print(f"{Colors.DIM}Recherche non disponible dans ce sous-menu.{Colors.RESET}") 179 time.sleep(1) 180 continue 181 182 try: 183 option_index = int(option_index) 184 if option_index - 1 in range(len(self.OPTIONS)): 185 # Appelle la fonction de l'option (ex: self.install) 186 ret_code = self.OPTIONS[option_index - 1][1]() 187 if ret_code != 99: 188 soft_input("\n↵ ENTER pour continuer…") 189 elif option_index == 98 and self.PROJECT_URL: 190 import webbrowser 191 webbrowser.open_new_tab(self.PROJECT_URL) 192 elif option_index == 99: 193 return 99 194 except (TypeError, ValueError): 195 print(f"{Colors.RED}Entrée invalide.{Colors.RESET}") 196 time.sleep(0.8) 197 except Exception as e: 198 print(f"{Colors.RED}Erreur inattendue: {e}{Colors.RESET}") 199 soft_input("\n↵ ENTER pour continuer…")
def
install(self):
202 def install(self): 203 self.before_install() 204 if not AppConfig.HOME_PATH: 205 print(f"{Colors.RED}Le chemin d'installation (HOME_PATH) n'est pas configuré.{Colors.RESET}") 206 return 1 207 208 print(f"Installation des outils dans : {AppConfig.HOME_PATH}") 209 for cmd in self.INSTALL_COMMANDS: 210 code = run_cmd(cmd, cwd=AppConfig.HOME_PATH) 211 if code != 0: 212 print(f"{Colors.RED}Échec de la commande: {cmd} (code: {code}){Colors.RESET}") 213 return code 214 self.after_install() 215 return 0
def
after_install(self):
216 def after_install(self): print(f"{Colors.GREEN}Installation terminée avec succès!{Colors.RESET}")
239class ClarityToolsCollection(ClarityTool): 240 TOOLS: List[object] = [] 241 242 def show_options(self, parent=None): 243 page = 0 244 per_page = 10 245 246 def filtered_tools(query: Optional[str]): 247 if not query: 248 return self.TOOLS 249 q = query.lower().strip() 250 return [t for t in self.TOOLS if q in getattr(t, "TITLE", "").lower() or q in getattr(t, "DESCRIPTION", "").lower()] 251 252 current_filter = None 253 while True: 254 clear() 255 menu() 256 title_line = f"{Colors.BOLD}{self.TITLE}{Colors.RESET} {badge('COLLECTION', Colors.GREEN)}" 257 if current_filter: 258 title_line += f" {badge('filter:'+current_filter, Colors.ORANGE)}" 259 print(title_line) 260 print(hr()) 261 262 tools = filtered_tools(current_filter) 263 if not tools: 264 print(f"{Colors.YELLOW}Aucun outil ne correspond à votre recherche.{Colors.RESET}") 265 else: 266 start = page * per_page 267 end = min(start + per_page, len(tools)) 268 for idx, tool in enumerate(tools[start:end], start=1): 269 print(f"{Colors.CYAN}[{idx}]{Colors.RESET} {tool.TITLE} {Colors.DIM}- {tool.DESCRIPTION or ''}{Colors.RESET}") 270 271 total_pages = max(1, (len(tools) + per_page - 1) // per_page) 272 print(hr()) 273 print(f"{Colors.GRAY}Page {page+1}/{total_pages} | 'n' suivant • 'p' précédent • 's' recherche • 'q' quitter{Colors.RESET}") 274 275 print(f"{Colors.YELLOW}[99]{Colors.RESET} Retour vers {parent.TITLE if parent else 'Quitter'}") 276 choice = soft_input(f"{badge('Choose')} ").strip().lower() 277 278 if not choice: # Ctrl+C 279 return 99 280 if choice == 'q': sys.exit(0) 281 if choice == 'n': 282 if tools and (page + 1) * per_page < len(tools): page += 1 283 continue 284 if choice == 'p': 285 if page > 0: page -= 1 286 continue 287 if choice == 's': 288 current_filter = soft_input("Rechercher: ").strip() 289 page = 0 290 continue 291 292 try: 293 if choice == '99': 294 return 99 295 296 choice_int = int(choice) 297 start = page * per_page 298 index_global = start + (choice_int - 1) 299 if 0 <= index_global < len(tools): 300 tools[index_global].show_options(parent=self) 301 else: 302 print(f"{Colors.RED}Choix invalide.{Colors.RESET}") 303 time.sleep(0.8) 304 except (TypeError, ValueError): 305 print(f"{Colors.RED}Entrée invalide.{Colors.RESET}") 306 time.sleep(0.8) 307 except Exception as e: 308 print(f"{Colors.RED}Erreur inattendue: {e}{Colors.RESET}") 309 soft_input("\n↵ ENTER pour continuer…")
def
show_options(self, parent=None):
242 def show_options(self, parent=None): 243 page = 0 244 per_page = 10 245 246 def filtered_tools(query: Optional[str]): 247 if not query: 248 return self.TOOLS 249 q = query.lower().strip() 250 return [t for t in self.TOOLS if q in getattr(t, "TITLE", "").lower() or q in getattr(t, "DESCRIPTION", "").lower()] 251 252 current_filter = None 253 while True: 254 clear() 255 menu() 256 title_line = f"{Colors.BOLD}{self.TITLE}{Colors.RESET} {badge('COLLECTION', Colors.GREEN)}" 257 if current_filter: 258 title_line += f" {badge('filter:'+current_filter, Colors.ORANGE)}" 259 print(title_line) 260 print(hr()) 261 262 tools = filtered_tools(current_filter) 263 if not tools: 264 print(f"{Colors.YELLOW}Aucun outil ne correspond à votre recherche.{Colors.RESET}") 265 else: 266 start = page * per_page 267 end = min(start + per_page, len(tools)) 268 for idx, tool in enumerate(tools[start:end], start=1): 269 print(f"{Colors.CYAN}[{idx}]{Colors.RESET} {tool.TITLE} {Colors.DIM}- {tool.DESCRIPTION or ''}{Colors.RESET}") 270 271 total_pages = max(1, (len(tools) + per_page - 1) // per_page) 272 print(hr()) 273 print(f"{Colors.GRAY}Page {page+1}/{total_pages} | 'n' suivant • 'p' précédent • 's' recherche • 'q' quitter{Colors.RESET}") 274 275 print(f"{Colors.YELLOW}[99]{Colors.RESET} Retour vers {parent.TITLE if parent else 'Quitter'}") 276 choice = soft_input(f"{badge('Choose')} ").strip().lower() 277 278 if not choice: # Ctrl+C 279 return 99 280 if choice == 'q': sys.exit(0) 281 if choice == 'n': 282 if tools and (page + 1) * per_page < len(tools): page += 1 283 continue 284 if choice == 'p': 285 if page > 0: page -= 1 286 continue 287 if choice == 's': 288 current_filter = soft_input("Rechercher: ").strip() 289 page = 0 290 continue 291 292 try: 293 if choice == '99': 294 return 99 295 296 choice_int = int(choice) 297 start = page * per_page 298 index_global = start + (choice_int - 1) 299 if 0 <= index_global < len(tools): 300 tools[index_global].show_options(parent=self) 301 else: 302 print(f"{Colors.RED}Choix invalide.{Colors.RESET}") 303 time.sleep(0.8) 304 except (TypeError, ValueError): 305 print(f"{Colors.RED}Entrée invalide.{Colors.RESET}") 306 time.sleep(0.8) 307 except Exception as e: 308 print(f"{Colors.RED}Erreur inattendue: {e}{Colors.RESET}") 309 soft_input("\n↵ ENTER pour continuer…")