# Annexe : Application - Navigateur Web

## Rôle et utilité

L'application `apps/navigateur/app.py` permet à l'utilisateur de surfer sur Internet. Cependant, un moteur de rendu HTML/CSS/JS complet étant bien trop lourd pour être codé entièrement en Python dans le cadre de ce projet, cette application agit plutôt comme une "enveloppe" (wrapper) autour d'un navigateur extrêmement léger existant sur Linux.

## Implémentation technique

- Encapsulation via X11 (Reparenting) : C'est la technique la plus spectaculaire de cette application. Le script lance le navigateur `surf` (un navigateur minimaliste basé sur WebKit) via `subprocess.Popen`. Mais plutôt que de le laisser s'afficher dans sa propre fenêtre flottante gérée par le serveur X, le script utilise des commandes X11 natives (comme `xdotool`) pour capturer la fenêtre de `surf` et l'incruster à l'intérieur de la zone Tkinter générée par GrimOS.
- Barre d'adresse (UI native) : La zone supérieure (barre d'adresse, boutons Précédent/Suivant) est codée en pur Tkinter. Lorsqu'on tape une URL, le script envoie un signal logiciel à l'enveloppe `surf` pour qu'il charge la page, donnant l'illusion d'une application unifiée.
- Presse-papiers asynchrone : Pour permettre les "Copier-Coller" entre la zone web (gérée par `surf`/X11) et l'environnement GrimOS (géré par Tkinter), un daemon silencieux en arrière-plan synchronise la mémoire X-Selection avec le presse-papiers interne de Python.

## Pistes de modification

- Changement de moteur : `surf` étant très basique, un développeur pourrait modifier ce fichier pour utiliser un moteur Python natif via `PyQtWebEngine` ou `tkinterweb`. Cela alourdirait l'application, mais offrirait un bien meilleur support du Javascript moderne et éviterait les complexités de l'encapsulation X11.
- Gestion des favoris (Bookmarks) : On pourrait facilement ajouter un fichier JSON local `~/.config/grimos_bookmarks.json` et des boutons d'étoiles pour permettre à l'utilisateur de sauvegarder ses sites préférés.

## Code Source

    import tkinter as tk
    import subprocess
    import os
    import json
    from urllib.parse import urlparse

    def load_bookmarks():
        path = os.path.join(os.path.dirname(__file__), '..', '..', 'config', 'bookmarks.json')
        try:
            with open(path, 'r', encoding='utf-8') as f:
                return json.load(f)
        except:
            return []

    def save_bookmarks(bookmarks):
        path = os.path.join(os.path.dirname(__file__), '..', '..', 'config', 'bookmarks.json')
        try:
            os.makedirs(os.path.dirname(path), exist_ok=True)
            with open(path, 'w', encoding='utf-8') as f:
                json.dump(bookmarks, f, indent=4)
        except Exception as e:
            print(f"Erreur favoris: {e}")

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

        icon_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "icons")
        window.icons = getattr(window, "icons", {})
        for iname in ['btn_arrow_left', 'btn_home', 'btn_rocket', 'btn_trash']:
            if iname not in window.icons:
                ipath = os.path.join(icon_dir, iname + ".png")
                if os.path.exists(ipath):
                    window.icons[iname] = tk.PhotoImage(file=ipath)

        top_bar = tk.Frame(main_frame, bg="#e0e0e0", height=40)
        top_bar.pack(side="top", fill="x", padx=5, pady=5)

        browser_proc = [None]

        bookmarks_btn = tk.Menubutton(top_bar, text="Favoris ▼", font=("Arial", 11, "bold"), relief="flat", bg="#e0e0e0", activebackground="#ccc", cursor="hand2")
        bookmarks_btn.pack(side="left", padx=5)

        def send_cmd(cmd_str):
            if browser_proc[0] is not None and browser_proc[0].poll() is None:
                try:
                    browser_proc[0].stdin.write(cmd_str.encode('utf-8'))
                    browser_proc[0].stdin.flush()
                except:
                    pass

        btn_back = tk.Button(top_bar, image=window.icons.get('btn_arrow_left'), relief="flat", bg="#e0e0e0", activebackground="#ccc", cursor="hand2", command=lambda: send_cmd("BACK\n"))
        btn_back.pack(side="left", padx=(5, 0))

        btn_home = tk.Button(top_bar, image=window.icons.get('btn_home'), relief="flat", bg="#e0e0e0", activebackground="#ccc", cursor="hand2", command=lambda: load_bookmark("https://duckduckgo.com"))
        btn_home.pack(side="left", padx=5)

        bookmarks_menu = tk.Menu(bookmarks_btn, tearoff=0)
        bookmarks_btn.config(menu=bookmarks_menu)

        url_var = tk.StringVar(value="https://duckduckgo.com")

        def refresh_bookmarks_menu():
            bookmarks_menu.delete(0, tk.END)
            bookmarks_menu.add_command(label="➕ Ajouter la page actuelle", command=add_current_bookmark)

            bookmarks = load_bookmarks()
            if bookmarks:
                del_menu = tk.Menu(bookmarks_menu, tearoff=0)
                for b in bookmarks:
                    del_menu.add_command(label=b['name'], command=lambda b_url=b['url']: delete_bookmark(b_url))
                bookmarks_menu.add_cascade(label=" Supprimer un favori...", image=window.icons.get('btn_trash'), compound="left", menu=del_menu)
                bookmarks_menu.add_separator()

                for b in bookmarks:
                    bookmarks_menu.add_command(label=b['name'], command=lambda b_url=b['url']: load_bookmark(b_url))

        def add_current_bookmark():
            url = url_var.get()
            if not url: return
            try:
                parsed = urlparse(url)
                name = parsed.netloc if parsed.netloc else url
                name = name.replace("www.", "")
            except:
                name = url

            bookmarks = load_bookmarks()
            if not any(b['url'] == url for b in bookmarks):
                bookmarks.append({"name": name, "url": url})
                save_bookmarks(bookmarks)
                refresh_bookmarks_menu()

        def delete_bookmark(url):
            bookmarks = load_bookmarks()
            bookmarks = [b for b in bookmarks if b['url'] != url]
            save_bookmarks(bookmarks)
            refresh_bookmarks_menu()

        def load_bookmark(url):
            url_var.set(url)
            launch_browser()

        refresh_bookmarks_menu()

        url_entry = tk.Entry(top_bar, textvariable=url_var, font=("Arial", 12))
        url_entry.pack(side="left", fill="x", expand=True, padx=(0, 5))

        def show_help():
            from tkinter import messagebox
            msg = (
                "Navigateur GrimOS :\n\n"
                "• Saisissez une URL (ex: duckduckgo.com) ou un chemin local (file://...) et faites Entrée.\n"
                "• Utilisez les boutons Retour et Accueil pour naviguer.\n"
                "• Cliquez sur le bouton 'Favoris ▼' pour ajouter ou retrouver vos sites préférés.\n\n"
                "Technique : Ce navigateur utilise WebKitGTK intégré de façon transparente dans l'interface GrimOS (via Overlay)."
            )
            messagebox.showinfo("Aide du Navigateur", msg)

        btn_help = tk.Button(top_bar, text=" ? Aide ", font=("Arial", 10), bg="#ccc", relief="flat", cursor="hand2", command=show_help)
        btn_help.pack(side="right", padx=5)

        # The container that the GTK window will overlay
        # pady=(0, 15) leaves a 15px gap at the bottom so the GrimOS resize grip is visible
        web_container = tk.Frame(main_frame, bg="#e0e0e0")
        web_container.pack(side="bottom", fill="both", expand=True, pady=(0, 15))

        lbl_inactive = tk.Label(web_container, text="Cliquez pour activer le navigateur", bg="#e0e0e0", fg="#888", font=("Arial", 12))
        lbl_inactive.pack(expand=True)

        def lift_window(event):
            window.master.lift()

        web_container.bind("<Button-1>", lift_window)
        lbl_inactive.bind("<Button-1>", lift_window)

        def track_browser_position():
            if browser_proc[0] is not None and browser_proc[0].poll() is None:
                try:
                    # Check Z-order to hide GTK when behind other windows
                    windows = window.master.master.winfo_children()
                    is_topmost = False
                    if windows and windows[-1] == window.master:
                        is_topmost = True

                    if is_topmost:
                        root_x = web_container.winfo_rootx()
                        root_y = web_container.winfo_rooty()
                        w = web_container.winfo_width()
                        h = web_container.winfo_height()

                        if w > 10 and h > 10:
                            send_cmd(f"MOVE {root_x} {root_y} {w} {h}\n")
                    else:
                        # Move off-screen when not active
                        send_cmd(f"MOVE -9999 -9999 10 10\n")
                except Exception:
                    pass
                window.after(30, track_browser_position)

        def launch_browser(event=None):
            url = url_var.get()
            if not url.startswith("http") and not url.startswith("file://"):
                url = "https://" + url
                url_var.set(url)

            if browser_proc[0] is not None and browser_proc[0].poll() is None:
                send_cmd(f"LOAD {url}\n")
                window.master.lift()
                return

            engine_script = os.path.join(os.path.dirname(__file__), "browser_engine.py")

            cmd = [
                'python3', engine_script,
                '--url', url
            ]

            browser_proc[0] = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=open("/tmp/webkit_browser.log", "w"))

            import threading
            def read_stdout():
                try:
                    for line in iter(browser_proc[0].stdout.readline, b''):
                        line_str = line.decode('utf-8').strip()
                        if line_str.startswith("URL "):
                            new_url = line_str[4:]
                            window.after(0, lambda: url_var.set(new_url))
                        elif line_str.startswith("DOWNLOAD_START "):
                            path = line_str[15:]
                            if app_manager and hasattr(app_manager, 'desktop'):
                                window.after(0, lambda p=path: app_manager.desktop.show_toast(f"Téléchargement démarré :\n{p}"))
                        elif line_str.startswith("DOWNLOAD_FINISH"):
                            if app_manager and hasattr(app_manager, 'desktop'):
                                window.after(0, lambda: app_manager.desktop.show_toast("Téléchargement terminé."))
                except:
                    pass

            threading.Thread(target=read_stdout, daemon=True).start()

            # Start tracking and overlaying the GTK window
            window.master.lift()
            track_browser_position()

        url_entry.bind("<Return>", launch_browser)

        btn_launch = tk.Button(top_bar, text="Aller", image=window.icons.get('btn_rocket'), compound="left", command=launch_browser, font=("Arial", 10, "bold"), bg="#2196F3", fg="white", relief="flat")
        btn_launch.pack(side="right")

        def on_destroy(event):
            if str(event.widget) == str(window) and browser_proc[0] is not None:
                try:
                    browser_proc[0].stdin.close() # This tells GTK to quit gracefully
                    browser_proc[0].kill()
                except:
                    pass

        window.bind("<Destroy>", on_destroy)

        if filepath:
            if filepath.startswith("http") or filepath.startswith("file://"):
                url_var.set(filepath)
            else:
                url_var.set(f"file://{os.path.abspath(filepath)}")

        window.after(200, launch_browser)