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

Pistes de modification

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)