# 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

```python
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)
```
