# Annexe : Application - Terminal

## Rôle et utilité

L'application `apps/terminal/app.py` offre un accès visuel à la ligne de commande à l'intérieur même de GrimOS. Idéal pour les développeurs, il permet d'exécuter des commandes Git, des pings, ou d'installer des paquets sans avoir à quitter l'environnement graphique via le bouton "Fermer la session".

## Implémentation technique

- Boucle d'Écoute Entrée/Sortie (I/O) : Créer un terminal graphique en Python est notoirement complexe. Plutôt que de coder un véritable émulateur pseudo-terminal (PTY) complet de zéro, l'application lance un processus `bash` en arrière-plan (`subprocess.Popen` avec `stdin=PIPE, stdout=PIPE`).
- Redirection d'affichage : Un `Thread` Python (processus léger parallèle) est lancé en arrière-plan pour "écouter" en continu les réponses du bash caché. Dès que bash répond (ex: à un `ls`), le Thread capture ce texte et l'injecte dans le composant d'affichage `tk.Text` de la fenêtre.
- Interception clavier : La saisie est gérée minutieusement. Si l'utilisateur appuie sur `Entrée`, le texte qu'il vient de taper est extrait et poussé dans le "tuyau" (pipe) d'entrée du bash caché, puis effacé de l'écran pour laisser place au résultat de la commande.

## Pistes de modification

- Support des programmes interactifs : L'implémentation actuelle via `subprocess` classique gère mal les programmes qui nécessitent des interactions continues au clavier (comme `nano`, `htop` ou la saisie d'un mot de passe SSH caché). Le remplacement du backend par une véritable implémentation pseudo-terminale (`pty.fork()` ou `os.openpty()`) rendrait ce terminal compatible à 100% avec les standards UNIX.
- Historique de commandes : Mémoriser les 50 dernières commandes tapées dans une liste et permettre de naviguer dedans avec les Flèches Haut/Bas, comme dans un vrai terminal Bash.

## Code Source

    import tkinter as tk
    import subprocess
    import threading
    import os
    import signal

    from tkinter import messagebox

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

        def show_help():
            msg = (
                "Aide du Terminal GrimOS\n\n"
                "• Ce terminal est un environnement émulé en Python.\n"
                "• Flèche Haut / Bas : Naviguer dans l'historique.\n"
                "• Le copier-coller standard (Ctrl+C / Ctrl+V) est supporté.\n"
                "• La commande 'sudo' est désactivée par sécurité. "
                "Utilisez 'Xterm' pour l'administration."
            )
            messagebox.showinfo("Aide Terminal", msg)

        btn_help = tk.Button(top_frame, text=" ? Aide ", command=show_help, relief="flat", bg="lightblue")
        btn_help.pack(side="right", padx=5, pady=2)

        text_area = tk.Text(window, bg="black", fg="white", font=("Consolas", 10), insertbackground="white")
        text_area.pack(fill="both", expand=True)

        current_process = None
        command_history = []
        history_index = 0

        if filepath and os.path.isdir(filepath):
            current_cwd = filepath
        else:
            current_cwd = os.path.expanduser("~")

        def print_prompt():
            home = os.path.expanduser("~")
            display_cwd = current_cwd
            if display_cwd.startswith(home):
                display_cwd = "~" + display_cwd[len(home):]

            text_area.insert(tk.END, f"geo@grimos:{display_cwd}$ ")
            text_area.mark_set("input_start", "insert")
            text_area.mark_gravity("input_start", "left")
            text_area.see(tk.END)

        print_prompt()
        text_area.focus_set()

        def on_key_press(event):
            nonlocal current_process, history_index

            # Ctrl+C
            if event.state & 4 and event.keysym.lower() == "c":
                if current_process is not None and current_process.poll() is None:
                    try:
                        os.killpg(os.getpgid(current_process.pid), signal.SIGTERM)
                    except Exception:
                        pass
                    text_area.config(state=tk.NORMAL)
                    text_area.insert(tk.END, "^C\n")
                    text_area.see(tk.END)
                    return "break"
                return

            # Allow copy/paste via keyboard
            if event.state & 4:
                return

            if event.keysym == "Return":
                command = text_area.get("input_start", "end-1c")
                text_area.mark_set("insert", tk.END)
                text_area.insert(tk.END, "\n")

                if command.strip():
                    if not command_history or command_history[-1] != command:
                        command_history.append(command)
                    history_index = len(command_history)

                    text_area.config(state=tk.DISABLED)
                    threading.Thread(target=execute_in_background, args=(command,), daemon=True).start()
                else:
                    print_prompt()
                return "break"

            elif event.keysym in ("BackSpace", "Left"):
                if text_area.compare("insert", "<=", "input_start"):
                    return "break"

            elif event.keysym == "Up":
                if command_history and history_index > 0:
                    history_index -= 1
                    text_area.delete("input_start", "end")
                    text_area.insert("input_start", command_history[history_index])
                return "break"

            elif event.keysym == "Down":
                if command_history and history_index < len(command_history):
                    history_index += 1
                    text_area.delete("input_start", "end")
                    if history_index < len(command_history):
                        text_area.insert("input_start", command_history[history_index])
                return "break"

            elif event.keysym in ("Prior", "Next"):
                return "break"

        def on_any_key(event):
            if event.char and event.keysym not in ("BackSpace", "Return", "Left", "Right", "Up", "Down"):
                if text_area.compare("insert", "<", "input_start"):
                    text_area.mark_set("insert", tk.END)

        def execute_in_background(command):
            nonlocal current_process, current_cwd

            cmd_stripped = command.strip()
            cmd_parts = cmd_stripped.split()

            # Interception de cd
            if cmd_parts and cmd_parts[0] == "cd" and "&&" not in cmd_stripped and ";" not in cmd_stripped:
                target_dir = cmd_parts[1] if len(cmd_parts) > 1 else os.path.expanduser("~")
                new_cwd = os.path.abspath(os.path.join(current_cwd, target_dir))
                if os.path.isdir(new_cwd):
                    current_cwd = new_cwd
                    window.after(0, append_output, "", "")
                else:
                    window.after(0, append_output, "", f"cd: {target_dir}: Aucun fichier ou dossier de ce type\n")
                return

            # Interdire sudo explicitement
            if cmd_stripped.startswith("sudo ") or cmd_stripped == "sudo":
                msg = "Erreur : L'usage de 'sudo' est désactivé dans ce terminal pour des raisons de sécurité et de stabilité.\n"
                msg += "Si vous avez besoin des droits administrateur, veuillez 'Fermer la session' depuis le Menu Démarrer pour retourner au vrai terminal Linux.\n"
                msg += "Une fois vos tâches terminées, retapez simplement 'startx'.\n"
                window.after(0, append_output, "", msg)
                return

            try:
                current_process = subprocess.Popen(
                    command,
                    shell=True,
                    cwd=current_cwd,
                    stdin=subprocess.DEVNULL,
                    stdout=subprocess.PIPE,
                    stderr=subprocess.PIPE,
                    text=True,
                    start_new_session=True # Detach from TTY so sudo fails immediately instead of hanging
                )
                stdout, stderr = current_process.communicate()
                window.after(0, append_output, stdout, stderr)
            except Exception as e:
                window.after(0, append_output, "", str(e))
            finally:
                current_process = None

        def append_output(stdout, stderr):
            text_area.config(state=tk.NORMAL)
            if stdout:
                text_area.insert(tk.END, stdout)
            if stderr:
                text_area.insert(tk.END, stderr)

            if (stdout and not stdout.endswith('\n')) or (stderr and not stderr.endswith('\n')):
                text_area.insert(tk.END, "\n")

            print_prompt()

        text_area.bind("<KeyPress>", on_key_press)
        text_area.bind("<Key>", on_any_key, add="+")