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.
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.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.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.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.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)