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.
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. surf pour qu'il charge la page, donnant l'illusion d'une application unifiée.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.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.~/.config/grimos_bookmarks.json et des boutons d'étoiles pour permettre à l'utilisateur de sauvegarder ses sites préférés.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)