# 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

```python
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="+")
```
