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

Pistes de modification

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