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.

HOME_PATH: Optional[str] = None
class Colors:
23class Colors:
24    RED = "\033[91m"
25    GREEN = "\033[92m"
26    YELLOW = "\033[93m"
27    CYAN = "\033[96m"
28    MAGENTA = "\033[95m"
29    RESET = "\033[0m"
30    BOLD = "\033[1m"
31    BLUE = "\033[94m"
32    ORANGE = "\033[38;5;208m"
33    DIM = "\033[2m"
34    GRAY = "\033[90m"
RED = '\x1b[91m'
GREEN = '\x1b[92m'
YELLOW = '\x1b[93m'
CYAN = '\x1b[96m'
MAGENTA = '\x1b[95m'
RESET = '\x1b[0m'
BOLD = '\x1b[1m'
BLUE = '\x1b[94m'
ORANGE = '\x1b[38;5;208m'
DIM = '\x1b[2m'
GRAY = '\x1b[90m'
def clear():
36def clear():
37    os.system('cls' if os.name == 'nt' else 'clear')
def hr(char='─', width=60, color='\x1b[90m'):
39def hr(char="─", width=60, color=Colors.GRAY):
40    return f"{color}{char * width}{Colors.RESET}"
def badge(text: str, color='\x1b[96m'):
42def badge(text: str, color=Colors.CYAN):
43    return f"{color}[{text}]{Colors.RESET}"
def soft_input(prompt: str) -> str:
45def soft_input(prompt: str) -> str:
46    try:
47        return input(prompt)
48    except (EOFError, KeyboardInterrupt):
49        print() # Nouvelle ligne après Ctrl+C
50        return ""
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):
111def ensure_dir(path: str):
112    try:
113        os.makedirs(path, exist_ok=True)
114    except OSError as e:
115        print(f"{Colors.RED}Impossible de créer le dossier {path}: {e}{Colors.RESET}", file=sys.stderr)
116        raise # Renvoyer l'exception peut être nécessaire pour arrêter le programme
def expand_path(p: str) -> str:
118def expand_path(p: str) -> str:
119    return os.path.normpath(os.path.abspath(os.path.expanduser(p)))
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")
TITLE: str = ''
DESCRIPTION: str = ''
INSTALL_COMMANDS: List[str] = []
UNINSTALL_COMMANDS: List[str] = []
RUN_COMMANDS: List[str] = []
OPTIONS: List[Tuple[str, Callable]] = []
PROJECT_URL: str = ''
def show_info(self):
154    def show_info(self):
155        self._print_card()
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 before_install(self):
201    def before_install(self): pass
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}")
def before_uninstall(self) -> bool:
218    def before_uninstall(self) -> bool: return True
def uninstall(self):
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()
def after_uninstall(self):
224    def after_uninstall(self): pass
def before_run(self):
226    def before_run(self): pass
def run(self):
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
def after_run(self):
237    def after_run(self): pass
class ClarityToolsCollection(ClarityTool):
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…")
TOOLS: List[object] = []
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…")