Annexe : Application - Explorateur

Rôle et utilité

apps/explorateur/app.py est l'application la plus complexe et la plus utilisée de GrimOS. C'est l'interface graphique qui permet de naviguer dans l'arborescence des fichiers du système Linux, d'ouvrir des documents avec les bonnes applications, et de se connecter aux réseaux.

Implémentation technique

Pistes de modification

Code Source

import os
import shutil
import tkinter as tk
from tkinter import messagebox, simpledialog
from tkinter import ttk
import subprocess

def start(window, app_manager=None, **kwargs):
    top_frame = tk.Frame(window, bg="lightgray")
    top_frame.pack(side="top", fill="x")

    icon_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "icons")
    window.icons = {}
    for iname in ['menu_new_folder', 'menu_new_file', 'menu_rename', 'menu_delete', 'icon_folder', 'icon_file_txt', 'icon_file_py', 'icon_file_img', 'icon_file_generic', 'btn_search', 'btn_network', 'btn_disconnect', 'btn_paste', 'btn_print', 'btn_refresh']:
        ipath = os.path.join(icon_dir, iname + ".png")
        if os.path.exists(ipath):
            window.icons[iname] = tk.PhotoImage(file=ipath)

    # Initialize Clipboard on Desktop
    if app_manager and not hasattr(app_manager.desktop, "file_clipboard"):
        app_manager.desktop.file_clipboard = {"action": None, "path": None}

    def go_up():
        current = path_var.get()
        parent = os.path.dirname(current)
        if parent != current:
            path_var.set(parent)
            refresh_list()

    path_var = tk.StringVar(value=os.path.expanduser("~"))
    path_entry = tk.Entry(top_frame, textvariable=path_var)
    path_entry.pack(side="left", fill="x", expand=True, padx=2, pady=2)

    def connect_network():
        pwd = app_manager.desktop.settings.get("sudo_pwd") if app_manager else None
        if not pwd:
            messagebox.showerror("Erreur", "Veuillez configurer votre mot de passe système dans Paramètres.")
            return

        dialog = tk.Toplevel(window)
        dialog.title("Connecter Réseau (SMB)")
        dialog.geometry("600x480")
        dialog.transient(window)
        dialog.grab_set()

        frame_top = tk.Frame(dialog)
        frame_top.pack(fill="both", expand=True, padx=10, pady=5)

        tk.Label(frame_top, text="Adresses découvertes sur le réseau :").pack(anchor="w")

        tree_frame = tk.Frame(frame_top)
        tree_frame.pack(fill="both", expand=True, pady=5)

        scroll = tk.Scrollbar(tree_frame)
        scroll.pack(side="right", fill="y")

        tree_shares = ttk.Treeview(tree_frame, show="tree", yscrollcommand=scroll.set, height=8)
        tree_shares.pack(side="left", fill="both", expand=True)
        scroll.config(command=tree_shares.yview)

        def scan_network():
            import socket
            import threading

            s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            try:
                s.connect(('10.255.255.255', 1))
                local_ip = s.getsockname()[0]
            except Exception:
                local_ip = "192.168.1.1"
            finally:
                s.close()

            subnet = ".".join(local_ip.split(".")[:3]) + "."
            found_ips = []

            def check_ip(ip):
                sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
                sock.settimeout(0.3)
                try:
                    if sock.connect_ex((ip, 445)) == 0:
                        found_ips.append(ip)
                except Exception: pass
                finally: sock.close()

            threads = []
            for i in range(1, 255):
                t = threading.Thread(target=check_ip, args=(subnet + str(i),))
                threads.append(t)
                t.start()

            for t in threads:
                t.join()

            # Now extract shares and hostnames using smbclient if installed
            detailed_shares = {}
            for ip in found_ips:
                hostname = ""
                try:
                    res_nmb = subprocess.run(['nmblookup', '-A', ip], capture_output=True, text=True, timeout=1)
                    if res_nmb.returncode == 0:
                        for line in res_nmb.stdout.split('\n'):
                            if '<00>' in line and 'GROUP' not in line:
                                hostname = line.split('<00>')[0].strip()
                                break
                except: pass

                display_name = f"{hostname} ({ip})" if hostname else f"[{ip}]"
                detailed_shares[display_name] = []

                try:
                    res_smb = subprocess.run(['smbclient', '-L', ip, '-N', '-g'], capture_output=True, text=True, timeout=2)
                    if res_smb.returncode == 0:
                        has_shares = False
                        for line in res_smb.stdout.split('\n'):
                            if line.startswith('Disk|'):
                                parts = line.split('|')
                                if len(parts) >= 2:
                                    share_name = parts[1]
                                    if share_name not in ('print$', 'IPC$'):
                                        detailed_shares[display_name].append(f"//{ip}/{share_name}")
                                        has_shares = True
                        if not has_shares:
                            detailed_shares[display_name].append(f"//{ip}/Partage")
                    else:
                        detailed_shares[display_name].append(f"//{ip}/Partage")
                except:
                    detailed_shares[display_name].append(f"//{ip}/Partage")

            return detailed_shares

        def on_scan(auto=False):
            btn_scan.config(state="disabled", text="Recherche...")
            dialog.update()
            shares = scan_network()
            btn_scan.config(state="normal", text=" Actualiser", image=window.icons.get('btn_search'), compound="left")
            tree_shares.delete(*tree_shares.get_children())
            if shares:
                icon_folder = window.icons.get('icon_folder')
                for machine, share_list in shares.items():
                    machine_id = tree_shares.insert("", "end", text=f" {machine}", open=False)
                    for share in share_list:
                        share_name = share.split('/')[-1]

                        # Check if already mounted
                        parts = share.strip("/").split("/")
                        if len(parts) >= 2:
                            ip_addr = parts[0]
                            mount_p = os.path.expanduser(f"~/Réseau/{ip_addr}/{share_name}")
                            disp_text = f" {share_name} (Connecté ✅)" if os.path.ismount(mount_p) else f" {share_name}"
                        else:
                            disp_text = f" {share_name}"

                        if icon_folder:
                            tree_shares.insert(machine_id, "end", text=disp_text, image=icon_folder, values=(share,))
                        else:
                            tree_shares.insert(machine_id, "end", text=disp_text, values=(share,))
                first = tree_shares.get_children()[0]
                tree_shares.selection_set(first)
                on_tree_select(None)
                if not auto:
                    msg = f"{len(shares)} partage(s) trouvé(s) sur le réseau !"
                    if not shutil.which("smbclient"):
                        msg += "\n\n(Note: Installez 'smbclient' via le terminal système pour afficher les noms exacts des dossiers)."
                    messagebox.showinfo("Scanner", msg)
            else:
                if not auto:
                    messagebox.showinfo("Scanner", "Aucune machine trouvée.")

        def on_tree_select(e):
            selected = tree_shares.selection()
            if selected:
                item = tree_shares.item(selected[0])
                if item.get('values'):
                    share_path = item['values'][0]
                    try:
                        machine_name = tree_shares.item(tree_shares.parent(selected[0]))['text'].strip()
                        entry_share_var.set(f"{machine_name} -> {share_path}")
                    except:
                        entry_share_var.set(share_path)

        tree_shares.bind("<<TreeviewSelect>>", on_tree_select)

        btn_scan = tk.Button(frame_top, text=" Actualiser", image=window.icons.get('btn_search'), compound="left", command=on_scan, bg="#ffeb3b")
        btn_scan.pack(anchor="e", pady=5)

        tk.Label(dialog, text="Adresse du partage sélectionné (ex: NAS -> //192.168.1.10/Data):").pack(pady=(10,0))
        entry_share_var = tk.StringVar()
        entry_share = tk.Entry(dialog, textvariable=entry_share_var, width=50)
        entry_share.pack(pady=5)

        tk.Label(dialog, text="Nom d'utilisateur (laisser vide si invité) :").pack(pady=5)
        entry_user = tk.Entry(dialog, width=30)
        entry_user.pack(pady=5)

        tk.Label(dialog, text="Mot de passe :").pack(pady=5)
        entry_pass = tk.Entry(dialog, width=30, show="*")
        entry_pass.pack(pady=5)

        def on_connect():
            share_input = entry_share_var.get().strip()
            user = entry_user.get()
            passw = entry_pass.get()
            if not share_input: return

            if " -> " in share_input:
                machine_str, share = share_input.split(" -> ", 1)
            else:
                machine_str, share = "", share_input

            if not share.startswith("//"):
                messagebox.showerror("Erreur", "Le format doit être //IP/Partage")
                return

            share_parts = share.strip("/").split("/")
            if len(share_parts) < 2:
                messagebox.showerror("Erreur", "Veuillez préciser le nom du dossier à la fin (ex: //192.168.1.10/Documents)")
                return

            server_ip = share_parts[0]
            share_name = share_parts[1]

            machine_clean = machine_str.split(" (")[0]
            machine_clean = "".join(c for c in machine_clean if c.isalnum() or c in ('-','_','.')).strip()
            machine_dir = f"{machine_clean}_{server_ip}" if machine_clean and machine_clean.lower() != "inconnu" else server_ip

            mount_point = os.path.expanduser(f"~/Réseau/{machine_dir}/{share_name}")
            os.makedirs(mount_point, exist_ok=True)

            if os.path.ismount(mount_point):
                messagebox.showinfo("Information", f"Ce partage est déjà connecté dans :\n{mount_point}")
                path_var.set(mount_point)
                refresh_list()
                dialog.destroy()
                return

            opts = f"username={user},password={passw}" if user else "guest"
            cmd = f"mount -t cifs '{share}' '{mount_point}' -o {opts},uid=$(id -u),gid=$(id -g)"

            res = subprocess.run(['sudo', '-S', 'bash', '-c', cmd], input=pwd+'\n', capture_output=True, text=True)
            if res.returncode == 0:
                messagebox.showinfo("Succès", f"Partage monté dans :\n{mount_point}")
                path_var.set(mount_point)
                refresh_list()
                dialog.destroy()
            else:
                messagebox.showerror("Erreur", f"Échec du montage:\n{res.stderr}\n\nAvez-vous installé cifs-utils ?")

        btn_frame = tk.Frame(dialog)
        btn_frame.pack(pady=15)

        tk.Button(btn_frame, text="Annuler", command=dialog.destroy).pack(side="left", padx=5)
        tk.Button(btn_frame, text="Connecter", bg="lightgreen", command=on_connect).pack(side="left", padx=5)

        dialog.bind("<Escape>", lambda e: dialog.destroy())

        dialog.after(200, lambda: on_scan(auto=True))

    btn_network = tk.Button(top_frame, text=" Réseau", image=window.icons.get('btn_network'), compound="left", relief="flat", bg="lightblue", command=connect_network)
    btn_network.pack(side="right", padx=5, pady=5)

    def umount_all():
        pwd = app_manager.desktop.settings.get("sudo_pwd") if app_manager else None
        if not pwd: return
        reseau_dir = os.path.expanduser("~/Réseau")
        if not os.path.exists(reseau_dir): return

        has_unmounted = False
        for root, dirs, files in os.walk(reseau_dir, topdown=False):
            for d in dirs:
                full_path = os.path.join(root, d)
                if os.path.ismount(full_path):
                    subprocess.run(['sudo', '-S', 'umount', full_path], input=pwd+'\n', text=True)
                    has_unmounted = True

        subprocess.run(['sudo', '-S', 'find', reseau_dir, '-type', 'd', '-empty', '-delete'], input=pwd+'\n', text=True)
        if has_unmounted and app_manager:
            app_manager.desktop.show_toast("Tous les partages ont été déconnectés.")

        def finish_umount():
            if path_var.get().startswith(os.path.expanduser("~/Réseau")):
                path_var.set(os.path.expanduser("~"))
            refresh_list()

        window.after(500, finish_umount)

    btn_umount = tk.Button(top_frame, text=" Tout démonter", image=window.icons.get('btn_disconnect'), compound="left", relief="flat", bg="#ffcccc", command=umount_all)
    btn_umount.pack(side="right", padx=5, pady=5)

    btn_go = tk.Button(top_frame, text="Aller", relief="flat", command=lambda: refresh_list())
    btn_go.pack(side="right", padx=5, pady=5)

    show_hidden = tk.BooleanVar(value=False)

    list_frame = tk.Frame(window)
    list_frame.pack(fill="both", expand=True)

    scrollbar = tk.Scrollbar(list_frame)
    scrollbar.pack(side="right", fill="y")

    style = ttk.Style()
    style.configure("Explorateur.Treeview", rowheight=24, font=("Arial", 10))

    tree = ttk.Treeview(list_frame, show="tree", yscrollcommand=scrollbar.set, style="Explorateur.Treeview")
    tree.pack(side="left", fill="both", expand=True)
    scrollbar.config(command=tree.yview)

    def refresh_list(event=None):
        current_path = path_var.get()
        if not os.path.exists(current_path) or not os.path.isdir(current_path):
            return

        tree.delete(*tree.get_children())
        tree.insert("", "end", iid="..", text="   .. (Dossier parent)", image=window.icons.get('icon_folder'))

        try:
            items = os.listdir(current_path)
            items.sort(key=lambda x: (not os.path.isdir(os.path.join(current_path, x)), x.lower()))
            for item in items:
                if not show_hidden.get() and item.startswith('.'):
                    continue
                is_dir = os.path.isdir(os.path.join(current_path, item))
                if is_dir:
                    icon = window.icons.get('icon_folder')
                else:
                    ext = os.path.splitext(item)[1].lower()
                    if ext in ['.png', '.jpg', '.jpeg', '.gif', '.bmp']:
                        icon = window.icons.get('icon_file_img')
                    elif ext in ['.txt', '.md', '.log', '.csv']:
                        icon = window.icons.get('icon_file_txt')
                    elif ext in ['.py']:
                        icon = window.icons.get('icon_file_py')
                    else:
                        icon = window.icons.get('icon_file_generic')
                tree.insert("", "end", iid=item, text="   " + item, image=icon)
        except PermissionError:
            tree.insert("", "end", iid="ACCESS_DENIED", text="   <Accès refusé>", image=window.icons.get('icon_file_generic'))

    def get_selected_item_path(item_id):
        if not item_id or item_id == ".." or item_id == "ACCESS_DENIED":
            return None
        return os.path.join(path_var.get(), item_id)

    def on_double_click(event):
        selection = tree.selection()
        if not selection:
            return

        item_id = selection[0]
        current_path = path_var.get()

        if item_id == "..":
            new_path = os.path.dirname(current_path)
            path_var.set(new_path)
            refresh_list()
        elif item_id == "ACCESS_DENIED":
            return
        else:
            new_path = os.path.join(current_path, item_id)
            if os.path.isdir(new_path):
                path_var.set(new_path)
                refresh_list()
            else:
                ext = os.path.splitext(item_id)[1].lower()
                if ext in ['.png', '.jpg', '.jpeg', '.gif', '.bmp']:
                    if app_manager:
                        app_manager.launch_app({"name": "Visionneuse", "module": "apps.visionneuse.app"}, filepath=new_path)
                elif ext in ['.html', '.htm']:
                    if app_manager:
                        app_manager.launch_app({"name": "Navigateur Web", "module": "apps.navigateur.app"}, filepath=new_path)
                elif ext in ['.py', '.json', '.sh', '.c', '.cpp', '.h', '.js', '.css', '.md']:
                    if app_manager:
                        app_manager.launch_app({"name": "Éditeur de Code", "module": "apps.editeur.app"}, filepath=new_path)
                else:
                    if app_manager:
                        app_manager.launch_app({"name": "Bloc-notes", "module": "apps.blocnotes.app"}, filepath=new_path)

    # --- Actions de gestion de fichiers ---
    def ask_string_dialog(title, prompt, callback, initialvalue=""):
        dialog_frame = tk.Frame(window, bg="white", highlightbackground="black", highlightthickness=2)
        dialog_frame.place(relx=0.5, rely=0.5, anchor="center", width=300, height=130)

        tk.Label(dialog_frame, text=title, bg="lightgray", font=("Arial", 10, "bold")).pack(fill="x")
        tk.Label(dialog_frame, text=prompt, bg="white").pack(pady=5)

        entry_var = tk.StringVar(value=initialvalue)
        entry = tk.Entry(dialog_frame, textvariable=entry_var)
        entry.pack(padx=10, fill="x")

        def on_ok(event=None):
            val = entry_var.get()
            dialog_frame.destroy()
            if val:
                callback(val)

        def on_cancel(event=None):
            dialog_frame.destroy()

        btn_frame = tk.Frame(dialog_frame, bg="white")
        btn_frame.pack(pady=10)
        tk.Button(btn_frame, text="OK", command=on_ok, width=8).pack(side="left", padx=5)
        tk.Button(btn_frame, text="Annuler", command=on_cancel, width=8).pack(side="left", padx=5)

        entry.bind("<Return>", on_ok)
        entry.bind("<Escape>", on_cancel)

        window.after(100, lambda: entry.focus_set())

    def new_file():
        def on_name(name):
            try:
                new_path = os.path.join(path_var.get(), name)
                if not os.path.exists(new_path):
                    open(new_path, 'a').close()
                    refresh_list()
                else:
                    messagebox.showerror("Erreur", "Un fichier avec ce nom existe déjà.")
            except Exception as e:
                messagebox.showerror("Erreur", f"Impossible de créer le fichier :\n{e}")
        ask_string_dialog("Nouveau Fichier", "Nom du nouveau fichier texte :", on_name)

    def new_folder():
        def on_name(name):
            try:
                new_path = os.path.join(path_var.get(), name)
                os.makedirs(new_path, exist_ok=False)
                refresh_list()
            except Exception as e:
                messagebox.showerror("Erreur", f"Impossible de créer le dossier :\n{e}")
        ask_string_dialog("Nouveau Dossier", "Nom du nouveau dossier :", on_name)

    def rename_item(item_id):
        old_path = get_selected_item_path(item_id)
        if not old_path: return
        old_name = os.path.basename(old_path)

        def on_name(new_name):
            if new_name != old_name:
                try:
                    new_path = os.path.join(os.path.dirname(old_path), new_name)
                    os.rename(old_path, new_path)
                    refresh_list()
                except Exception as e:
                    messagebox.showerror("Erreur", f"Impossible de renommer :\n{e}")

        ask_string_dialog("Renommer", "Nouveau nom :", on_name, initialvalue=old_name)

    def delete_item(item_id):
        target_path = get_selected_item_path(item_id)
        if not target_path: return

        if target_path.startswith(os.path.expanduser("~/Réseau")):
            messagebox.showerror("Interdit", "La suppression est interdite sur les partages réseau montés par sécurité.")
            return

        target_name = os.path.basename(target_path)
        is_dir = os.path.isdir(target_path)

        msg = f"Voulez-vous vraiment supprimer définitivement le {'dossier' if is_dir else 'fichier'} '{target_name}' ?"
        if is_dir:
            msg += "\n\nATTENTION: Tout le contenu du dossier sera supprimé !"

        if messagebox.askyesno("Confirmer la suppression", msg, icon='warning'):
            try:
                if is_dir:
                    shutil.rmtree(target_path)
                else:
                    os.remove(target_path)
                refresh_list()
            except Exception as e:
                messagebox.showerror("Erreur", f"Impossible de supprimer :\n{e}")

    def create_shortcut(item_id):
        target_path = get_selected_item_path(item_id)
        if not target_path: return

        target_name = os.path.basename(target_path)

        import json
        desktop_config_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "config", "desktop.json")
        try:
            icons = []
            if os.path.exists(desktop_config_path):
                with open(desktop_config_path, "r") as f:
                    icons = json.load(f)

            x = 20 + (len(icons) % 10) * 80
            y = 20 + (len(icons) // 10) * 80

            icons.append({"name": target_name, "path": target_path, "x": x, "y": y})

            with open(desktop_config_path, "w") as f:
                json.dump(icons, f, indent=4)

            if app_manager and hasattr(app_manager, "desktop"):
                app_manager.desktop.load_desktop_icons()
                app_manager.desktop.show_toast(f"Raccourci '{target_name}' créé.")
        except Exception as e:
            messagebox.showerror("Erreur", f"Impossible de créer le raccourci :\n{e}")

    def print_item(item_id):
        target_path = get_selected_item_path(item_id)
        if not target_path or os.path.isdir(target_path): return

        try:
            res = subprocess.run(['lp', target_path], capture_output=True, text=True)
            if res.returncode == 0:
                if app_manager and hasattr(app_manager, "desktop"):
                    app_manager.desktop.show_toast(f"Impression lancée pour '{os.path.basename(target_path)}'")
                else:
                    messagebox.showinfo("Impression", f"Fichier envoyé à l'imprimante :\n{os.path.basename(target_path)}")
            else:
                messagebox.showerror("Erreur Impression", f"Impossible d'imprimer:\n{res.stderr}\n\nAvez-vous configuré une imprimante par défaut ?")
        except FileNotFoundError:
            messagebox.showerror("Erreur", "La commande 'lp' est introuvable. Installez 'cups' pour imprimer.")

    def copy_item(item_id):
        if not app_manager: return
        target_path = get_selected_item_path(item_id)
        if target_path:
            app_manager.desktop.file_clipboard = {"action": "copy", "path": target_path}
            app_manager.desktop.show_toast(f"Copié : {os.path.basename(target_path)}")

    def cut_item(item_id):
        if not app_manager: return
        target_path = get_selected_item_path(item_id)
        if target_path:
            app_manager.desktop.file_clipboard = {"action": "cut", "path": target_path}
            app_manager.desktop.show_toast(f"Coupé : {os.path.basename(target_path)}")

    def paste_item():
        if not app_manager or not hasattr(app_manager.desktop, "file_clipboard"): return
        clip = app_manager.desktop.file_clipboard
        if not clip.get("path"): return

        src = clip["path"]
        action = clip["action"]
        dst_dir = path_var.get()
        dst = os.path.join(dst_dir, os.path.basename(src))

        if not os.path.exists(src):
            messagebox.showerror("Erreur", "Le fichier source n'existe plus.")
            return

        if os.path.exists(dst):
            messagebox.showerror("Erreur", "Un fichier ou dossier de ce nom existe déjà ici.")
            return

        try:
            if action == "copy":
                if os.path.isdir(src):
                    shutil.copytree(src, dst)
                else:
                    shutil.copy2(src, dst)
                app_manager.desktop.show_toast(f"Collé : {os.path.basename(src)}")
            elif action == "cut":
                shutil.move(src, dst)
                app_manager.desktop.file_clipboard = {"action": None, "path": None}
                app_manager.desktop.show_toast(f"Déplacé : {os.path.basename(src)}")
            refresh_list()
        except Exception as e:
            messagebox.showerror("Erreur", f"Erreur :\n{e}")

    def disconnect_network(m_path):
        pwd = app_manager.desktop.settings.get("sudo_pwd") if app_manager else None
        if not pwd: return

        has_unmounted = False
        if os.path.ismount(m_path):
            subprocess.run(['sudo', '-S', 'umount', m_path], input=pwd+'\n', text=True)
            has_unmounted = True
        else:
            for root, dirs, files in os.walk(m_path, topdown=False):
                for d in dirs:
                    full_p = os.path.join(root, d)
                    if os.path.ismount(full_p):
                        subprocess.run(['sudo', '-S', 'umount', full_p], input=pwd+'\n', text=True)
                        has_unmounted = True

        if has_unmounted:
            subprocess.run(['sudo', '-S', 'find', m_path, '-type', 'd', '-empty', '-delete'], input=pwd+'\n', text=True)
            if not os.path.exists(m_path):
                # Clean up parent if it became empty
                parent = os.path.dirname(m_path)
                subprocess.run(['sudo', '-S', 'find', parent, '-type', 'd', '-empty', '-delete'], input=pwd+'\n', text=True)

            if app_manager: app_manager.desktop.show_toast(f"Déconnecté : {os.path.basename(m_path)}")

            def finish_disconnect():
                if not os.path.exists(path_var.get()):
                    path_var.set(os.path.expanduser("~/Réseau"))
                refresh_list()

            window.after(500, finish_disconnect)
        else:
            messagebox.showerror("Erreur", "Aucun point de montage actif trouvé ici.")

    # --- Menu Contextuel ---
    context_menu = tk.Menu(window, tearoff=0)

    def show_context_menu(event):
        item_id = tree.identify_row(event.y)

        tree.selection_remove(tree.selection())
        context_menu.delete(0, tk.END)

        if item_id:
            tree.selection_set(item_id)
            target_path = get_selected_item_path(item_id)

            if item_id not in ("..", "ACCESS_DENIED"):
                context_menu.add_command(label=" Copier", command=lambda: window.after(50, copy_item, item_id))

                if not target_path.startswith(os.path.expanduser("~/Réseau")):
                    context_menu.add_command(label=" Couper", command=lambda: window.after(50, cut_item, item_id))
                    context_menu.add_separator()
                    context_menu.add_command(label=" Renommer", image=window.icons.get('menu_rename'), compound="left", command=lambda: window.after(50, rename_item, item_id))
                    context_menu.add_command(label=" Supprimer", image=window.icons.get('menu_delete'), compound="left", command=lambda: window.after(50, delete_item, item_id))
                else:
                    context_menu.add_separator()

                context_menu.add_command(label=" Raccourci Bureau", command=lambda: window.after(50, create_shortcut, item_id))

                if target_path and os.path.isfile(target_path):
                    ext = os.path.splitext(target_path)[1].lower()
                    if ext in ['.txt', '.py', '.md', '.json', '.log', '.csv', '.png', '.jpg', '.jpeg']:
                        context_menu.add_command(label=" Imprimer", image=window.icons.get('btn_print'), compound="left", command=lambda: window.after(50, print_item, item_id))

                if target_path and target_path.startswith(os.path.expanduser("~/Réseau")) and target_path != os.path.expanduser("~/Réseau"):
                    rel_path = os.path.relpath(target_path, os.path.expanduser("~/Réseau"))
                    if len(rel_path.split(os.sep)) <= 2:
                        context_menu.add_command(label=" Déconnecter", image=window.icons.get('menu_delete'), compound="left", command=lambda p=target_path: window.after(50, disconnect_network, p))

                context_menu.add_separator()

        if app_manager and hasattr(app_manager.desktop, "file_clipboard") and app_manager.desktop.file_clipboard.get("path"):
            context_menu.add_command(label=" Coller", image=window.icons.get('btn_paste'), compound="left", command=lambda: window.after(50, paste_item))
            context_menu.add_separator()

        context_menu.add_command(label=" Nouveau dossier", image=window.icons.get('menu_new_folder'), compound="left", command=lambda: window.after(50, new_folder))
        context_menu.add_command(label=" Nouveau fichier texte", image=window.icons.get('menu_new_file'), compound="left", command=lambda: window.after(50, new_file))

        try:
            context_menu.tk_popup(event.x_root, event.y_root)
        finally:
            context_menu.grab_release()

    # Bindings
    tree.bind("<Double-Button-1>", on_double_click)
    tree.bind("<Button-3>", show_context_menu) # Clic droit

    # Fermer le menu si on clique ailleurs
    window.bind("<Button-1>", lambda e: context_menu.unpost(), add="+")
    tree.bind("<Button-1>", lambda e: context_menu.unpost(), add="+")

    path_entry.bind("<Return>", refresh_list)

    # Boutons d'accès rapide
    btn_go = tk.Button(top_frame, text="Aller", command=refresh_list, relief="flat")
    btn_go.pack(side="right", padx=2, pady=2)

    btn_term = tk.Button(top_frame, text=">_ Terminal", command=lambda: app_manager.launch_app({"name": "Terminal", "module": "apps.terminal.app"}, filepath=path_var.get()) if app_manager else None, relief="flat", bg="#e0e0e0")
    btn_term.pack(side="right", padx=5, pady=2)

    chk_hidden = tk.Checkbutton(top_frame, text="Fichiers cachés", variable=show_hidden, command=refresh_list, bg="lightgray")
    chk_hidden.pack(side="right", padx=5)

    def show_help():
        msg = (
            "Aide de l'Explorateur\n\n"
            "Navigation :\n"
            "• Double-cliquez sur un dossier pour y entrer.\n"
            "• Double-cliquez sur '.. (Dossier parent)' pour remonter.\n"
            "• Double-cliquez sur un fichier pour l'ouvrir.\n\n"
            "Actions :\n"
            "• Clic-droit sur un élément pour le renommer ou le supprimer.\n"
            "• Le menu Clic-droit permet aussi de créer des fichiers/dossiers."
        )
        messagebox.showinfo("Aide Explorateur", msg)

    btn_help = tk.Button(top_frame, text=" Aide", image=(app_manager.desktop.icons.get("btn_help") if app_manager else None), compound="left", command=show_help, relief="flat", bg="lightblue")
    btn_help.pack(side="right", padx=5, pady=2)

    refresh_list()