# Annexe : Application - Éditeur de Code

## Rôle et utilité

`apps/editeur/app.py` est une application capitale de l'écosystème GrimOS. Plus avancé que le simple Bloc-notes, c'est l'outil de développement officiel du système. Puisque GrimOS encourage l'utilisateur à modifier son propre code source, il était impératif de fournir un outil capable de lire du Python confortablement.

## Implémentation technique

- Coloration syntaxique "Fait-maison" : Contrairement aux IDE modernes qui utilisent d'énormes bibliothèques, cet éditeur utilise le système natif de "Tags" (balises) du composant `tk.Text`. Une fonction Python analyse le texte avec des expressions régulières (Regex) pour repérer les mots-clés (`def`, `class`, `import`), les chaînes de caractères (entre guillemets) ou les commentaires (débutant par `#`), et leur applique instantanément une couleur spécifique.
- Numérotation des lignes : Un deuxième `tk.Text` étroit est placé à gauche de la zone de texte principale. Un événement (`<KeyRelease>` ou `<MouseWheel>`) synchronise le défilement des deux zones pour maintenir les numéros de lignes alignés avec le code.
- Auto-indentation : Lorsqu'on appuie sur `Entrée`, l'éditeur vérifie le niveau d'indentation de la ligne précédente (le nombre d'espaces) et l'applique automatiquement à la nouvelle ligne, un confort indispensable en Python.

## Pistes de modification

- Lancement de script intégré : L'éditeur pourrait intégrer un bouton "Play" (Exécuter). En utilisant `subprocess.run()`, l'éditeur pourrait lancer le fichier Python actuellement ouvert dans une petite fenêtre terminal embarquée en bas de l'écran pour afficher le résultat de l'exécution en direct.
- Support d'autres langages : Actuellement codé en dur pour Python et JSON, le dictionnaire d'expressions régulières de coloration pourrait être extrait dans un fichier de configuration séparé, permettant à l'utilisateur d'ajouter les mots-clés du HTML, du C ou du Bash.

## Code Source

    import tkinter as tk
    from tkinter import filedialog, messagebox
    import re
    import os

    class LineNumbers(tk.Canvas):
        def __init__(self, *args, **kwargs):
            tk.Canvas.__init__(self, *args, **kwargs)
            self.textwidget = None

        def attach(self, text_widget):
            self.textwidget = text_widget

        def redraw(self, *args):
            self.delete("all")
            i = self.textwidget.index("@0,0")
            while True :
                dline= self.textwidget.dlineinfo(i)
                if dline is None: break
                y = dline[1]
                linenum = str(i).split(".")[0]
                self.create_text(2, y, anchor="nw", text=linenum, font=("Consolas", 10))
                i = self.textwidget.index("%s+1line" % i)

    def start(window, app_manager=None, filepath=None, **kwargs):
        frame = tk.Frame(window)
        frame.pack(fill="both", expand=True)

        toolbar = tk.Frame(frame, bg="lightgray", bd=1, relief="raised")
        toolbar.pack(side="top", fill="x")

        current_file = [filepath] if filepath else [None]

        text_frame = tk.Frame(frame)
        text_frame.pack(fill="both", expand=True)

        linenumbers = LineNumbers(text_frame, width=35, bg="lightgrey")
        linenumbers.pack(side="left", fill="y")

        scrollbar = tk.Scrollbar(text_frame)
        scrollbar.pack(side="right", fill="y")

        text_area = tk.Text(text_frame, font=("Consolas", 11), yscrollcommand=scrollbar.set, undo=True)
        text_area.pack(side="left", fill="both", expand=True)
        scrollbar.config(command=text_area.yview)

        linenumbers.attach(text_area)

        text_area.tag_configure("keyword", foreground="blue", font=("Consolas", 11, "bold"))
        text_area.tag_configure("string", foreground="green")
        text_area.tag_configure("comment", foreground="gray", font=("Consolas", 11, "italic"))

        KEYWORDS = ["def", "class", "import", "from", "return", "if", "elif", "else", "for", "while", "in", "and", "or", "not", "True", "False", "None", "try", "except", "pass", "break", "continue", "with", "as"]

        def highlight_syntax(event=None):
            text_area.tag_remove("keyword", "1.0", tk.END)
            text_area.tag_remove("string", "1.0", tk.END)
            text_area.tag_remove("comment", "1.0", tk.END)

            content = text_area.get("1.0", tk.END)

            # Strings
            for match in re.finditer(r'".*?"|\'.*?\'', content):
                start_pos = text_area.index(f"1.0 + {match.start()} chars")
                end_pos = text_area.index(f"1.0 + {match.end()} chars")
                text_area.tag_add("string", start_pos, end_pos)

            # Comments
            for match in re.finditer(r'#.*', content):
                start_pos = text_area.index(f"1.0 + {match.start()} chars")
                end_pos = text_area.index(f"1.0 + {match.end()} chars")
                text_area.tag_add("comment", start_pos, end_pos)

            # Keywords
            for kw in KEYWORDS:
                for match in re.finditer(rf'\b{kw}\b', content):
                    start_pos = text_area.index(f"1.0 + {match.start()} chars")
                    end_pos = text_area.index(f"1.0 + {match.end()} chars")
                    tags = text_area.tag_names(start_pos)
                    if "string" not in tags and "comment" not in tags:
                        text_area.tag_add("keyword", start_pos, end_pos)

        def on_text_change(event=None):
            if text_area.edit_modified() or event is not None:
                linenumbers.redraw()
                if hasattr(window, 'highlight_after'):
                    window.after_cancel(window.highlight_after)
                window.highlight_after = window.after(500, highlight_syntax)
                text_area.edit_modified(False)

        text_area.bind("<<Modified>>", lambda event: on_text_change())
        text_area.bind("<KeyRelease>", on_text_change)
        text_area.bind("<MouseWheel>", lambda e: window.after(10, linenumbers.redraw))
        text_area.bind("<Button-1>", lambda e: window.after(10, linenumbers.redraw))

        def auto_indent(event):
            line = text_area.get("insert linestart", "insert")
            match = re.match(r'^([ \t]+)', line)
            if match:
                text_area.insert("insert", "\n" + match.group(1))
                return "break"

        text_area.bind("<Return>", auto_indent)

        def update_title(title_text):
            if hasattr(window, 'master') and window.master.__class__.__name__ == 'Window':
                window.master.title_label.config(text=title_text)

        if current_file[0] and os.path.exists(current_file[0]):
            try:
                with open(current_file[0], 'r', encoding='utf-8') as f:
                    text_area.insert("1.0", f.read())
                update_title(f"Éditeur de Code - {os.path.basename(current_file[0])}")
                highlight_syntax()
                linenumbers.redraw()
            except Exception as e:
                messagebox.showerror("Erreur", str(e))
        else:
            update_title("Éditeur de Code - Nouveau Fichier")

        def open_file():
            path = filedialog.askopenfilename()
            if path:
                current_file[0] = path
                try:
                    with open(path, 'r', encoding='utf-8') as f:
                        text_area.delete("1.0", tk.END)
                        text_area.insert("1.0", f.read())
                    update_title(f"Éditeur de Code - {os.path.basename(path)}")
                    highlight_syntax()
                    linenumbers.redraw()
                except Exception as e:
                    messagebox.showerror("Erreur", str(e))

        def save_file():
            if not current_file[0]:
                save_as_file()
            else:
                try:
                    with open(current_file[0], 'w', encoding='utf-8') as f:
                        f.write(text_area.get("1.0", tk.END))
                    messagebox.showinfo("Sauvegarde", "Fichier sauvegardé avec succès.")
                except Exception as e:
                    messagebox.showerror("Erreur", str(e))

        def save_as_file():
            path = filedialog.asksaveasfilename(defaultextension=".py")
            if path:
                current_file[0] = path
                save_file()
                update_title(f"Éditeur de Code - {os.path.basename(path)}")

        def new_file():
            current_file[0] = None
            text_area.delete("1.0", tk.END)
            update_title("Éditeur de Code - Nouveau Fichier")
            highlight_syntax()
            linenumbers.redraw()

        def new_gui_file():
            new_file()
            boilerplate = '''import tkinter as tk
    from tkinter import messagebox

    def start(window, app_manager=None, filepath=None, **kwargs):
        # Changement du titre de la fenêtre GrimOS (si exécuté dans GrimOS)
        if hasattr(window, "master") and hasattr(window.master, "title_label"):
            window.master.title_label.config(text="Mon App GrimOS")

        # Conteneur principal
        frame = tk.Frame(window, bg="white")
        frame.pack(fill="both", expand=True, padx=10, pady=10)

        # Instructions
        tk.Label(frame, text="Saisissez un texte :", font=("Arial", 12), bg="white").pack(pady=(10, 0))

        # Zone de saisie (1 ligne)
        entry_var = tk.StringVar()
        entry = tk.Entry(frame, textvariable=entry_var, font=("Arial", 12), width=30)
        entry.pack(pady=10)

        def on_click():
            texte = entry_var.get()
            messagebox.showinfo("Popup", f"Texte saisi : {texte}")

        # Bouton d'action
        btn = tk.Button(frame, text="Afficher le Popup", command=on_click, font=("Arial", 11))
        btn.pack(pady=10)

    # Point d'entrée pour tester l'application indépendamment de GrimOS
    if __name__ == "__main__":
        root = tk.Tk()
        root.geometry("400x300")

        # En environnement GrimOS (bare X11 sans gestionnaire de fenêtres), tk.Tk n'a pas de bordures.
        # Nous ajoutons une fausse barre de titre pour pouvoir fermer et tester l'application sereinement.
        title_bar = tk.Frame(root, bg="darkblue", relief="raised", bd=1)
        title_bar.pack(fill="x")
        tk.Label(title_bar, text="Test Mode - Mon App", bg="darkblue", fg="white", font=("Arial", 10, "bold")).pack(side="left", padx=5)
        tk.Button(title_bar, text="X", bg="red", fg="white", command=root.destroy, bd=0, padx=5).pack(side="right")

        # Zone de contenu
        content = tk.Frame(root)
        content.pack(fill="both", expand=True)

        start(content)
        root.mainloop()
    '''
            text_area.insert("1.0", boilerplate)
            highlight_syntax()
            linenumbers.redraw()

        def show_help():
            aide = "Éditeur de Code GrimOS\n\n"
            aide += "COMPATIBILITÉ GRIMOS (Règles d'Or) :\n"
            aide += "1. Votre code DOIT avoir une fonction `start(window, app_manager=None, **kwargs)`.\n"
            aide += "2. L'argument `window` fourni est un Frame Tkinter pré-créé par le système. "
            aide += "Attachez TOUS vos éléments visuels (Boutons, Labels) à `window`.\n"
            aide += "3. Ne créez JAMAIS de `tk.Tk()` dans la fonction `start()`, cela casserait le système.\n\n"
            aide += "CONSEILS :\n"
            aide += "- Utilisez le bouton 'Nouveau GUI' pour générer une structure parfaite.\n"
            aide += "- Le bloc `if __name__ == '__main__':` sert uniquement à tester l'application en standalone."
            messagebox.showinfo("Aide & Documentation", aide)

        btn_new = tk.Button(toolbar, text=" Nouveau", image=(app_manager.desktop.icons.get("menu_new_file") if app_manager else None), compound="left", command=new_file)
        btn_new.pack(side="left", padx=2, pady=2)

        btn_new_gui = tk.Button(toolbar, text="Nouveau GUI", command=new_gui_file)
        btn_new_gui.pack(side="left", padx=2, pady=2)

        btn_open = tk.Button(toolbar, text=" Ouvrir", image=(app_manager.desktop.icons.get("menu_open") if app_manager else None), compound="left", command=open_file)
        btn_open.pack(side="left", padx=2, pady=2)

        btn_save = tk.Button(toolbar, text=" Enregistrer", image=(app_manager.desktop.icons.get("menu_save") if app_manager else None), compound="left", command=save_file)
        btn_save.pack(side="left", padx=2, pady=2)

        btn_saveas = tk.Button(toolbar, text=" Enregistrer sous...", image=(app_manager.desktop.icons.get("menu_save_as") if app_manager else None), compound="left", command=save_as_file)
        btn_saveas.pack(side="left", padx=2, pady=2)

        btn_help = tk.Button(toolbar, text=" Aide", image=(app_manager.desktop.icons.get("btn_help") if app_manager else None), compound="left", command=show_help)
        btn_help.pack(side="right", padx=2, pady=2)

        window.after(100, linenumbers.redraw)