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".
bash en arrière-plan (subprocess.Popen avec stdin=PIPE, stdout=PIPE). 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.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.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.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="+")