Si main.py allume la lumière, desktop.py est l'âme du système. Ce fichier est le composant le plus volumineux et le plus critique de GrimOS. Il dessine l'environnement visuel complet (Bureau, Barre des tâches, Menu Démarrer) et gère les processus vitaux en arrière-plan.
Desktop instancie de multiples conteneurs (tk.Frame). Le fond de l'écran est un grand Canvas (pour potentiellement y dessiner des icônes), tandis que le bas de l'écran est réservé à la barre des tâches.category, et construit des sous-menus Tkinter à la volée. check_power() ou update_status(). Elle utilise la méthode window.after(1000, fonction) de Tkinter pour s'exécuter toutes les secondes. C'est cette boucle qui lit les fichiers /sys/class/power_supply pour mettre à jour la batterie, lit /proc/stat pour calculer la charge CPU, et appelle lsblk pour détecter l'insertion d'une clé USB.lsblk -J, desktop.py déclenche la commande sudo mount silencieusement et fait apparaître le bouton d'éjection (⏏️).Canvas inerte. Pour le rendre pleinement interactif, on pourrait y ajouter la création de raccourcis (tk.Label avec images) dotés de la fonctionnalité "cliquer-glisser" en écoutant les événements <Button-1> et <B1-Motion>.import tkinter as tk
from tkinter import simpledialog
import subprocess
from time import strftime
import sys
import os
from core.app_manager import AppManager
from core.config import load_applications
class ToolTip:
def __init__(self, widget, text):
self.widget = widget
self.text = text
self.tooltip_window = None
self.widget.bind("<Enter>", self.show_tooltip)
self.widget.bind("<Leave>", self.hide_tooltip)
def show_tooltip(self, event=None):
if self.tooltip_window or not self.text:
return
x = self.widget.winfo_rootx()
y = self.widget.winfo_rooty() - 25
self.tooltip_window = tw = tk.Toplevel(self.widget)
tw.wm_overrideredirect(True)
tw.wm_geometry(f"+{x}+{y}")
label = tk.Label(tw, text=self.text, justify='left',
background="#ffffe0", relief='solid', borderwidth=1,
font=("Arial", "9", "normal"))
label.pack(ipadx=2, ipady=1)
def hide_tooltip(self, event=None):
tw = self.tooltip_window
self.tooltip_window = None
if tw:
tw.destroy()
class Desktop(tk.Frame):
def __init__(self, parent, settings):
super().__init__(parent)
self.parent = parent
self.settings = settings
self.pack(fill="both", expand=True)
self.app_manager = AppManager(self)
self.apps = load_applications()
self.toasts = []
# Desktop area
import sys, os
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from core.theme import THEMES
theme_name = self.settings.get("theme", "GrimOS")
self.theme_data = THEMES.get(theme_name, THEMES["GrimOS"])
bg_color = self.theme_data.get("desktop_bg", "gray")
# Taskbar
self.taskbar = tk.Frame(self, height=40, bg=self.theme_data.get("taskbar_bg", "#333"))
self.taskbar.pack(side="bottom", fill="x")
self.taskbar.pack_propagate(False)
self.desktop_frame = tk.Frame(self, bg=bg_color)
self.desktop_frame.pack(fill="both", expand=True)
self.load_desktop_icons()
# Load icons
self.icons = {}
icon_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "icons")
icon_theme = self.theme_data.get("icon_theme", "grimos")
icon_theme_dir = os.path.join(icon_dir, "themes", icon_theme)
for iname in ['menu_poweroff', 'menu_reboot', 'menu_logoff', 'btn_apply', 'btn_refresh', 'btn_kill', 'btn_start', 'btn_wifi', 'btn_usb', 'btn_help', 'menu_grimoire', 'btn_audio', 'btn_trash']:
ipath = os.path.join(icon_theme_dir, iname + ".png")
if not os.path.exists(ipath):
ipath = os.path.join(icon_dir, iname + ".png")
if os.path.exists(ipath):
self.icons[iname] = tk.PhotoImage(file=ipath)
# Load app icons
self.app_icons = {}
for app in self.apps:
icon_path = app.get("icon")
if icon_path:
full_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), icon_path)
if os.path.exists(full_path):
self.app_icons[app.get("module")] = tk.PhotoImage(file=full_path)
# Start button
self.start_btn = tk.Button(self.taskbar, image=self.icons.get("btn_start"),
bg=self.theme_data.get("start_btn_bg", "blue"),
fg=self.theme_data.get("start_btn_fg", "white"),
command=self.toggle_start_menu)
self.start_btn.pack(side="left", padx=5, pady=5)
ToolTip(self.start_btn, "Démarrer")
# Taskbar window buttons container
self.taskbar_windows = tk.Frame(self.taskbar, bg=self.theme_data.get("taskbar_bg", "#333"))
self.taskbar_windows.pack(side="left", fill="both", expand=True, padx=5)
self.taskbar_btns = {}
# Clock
self.clock_lbl = tk.Label(self.taskbar, bg=self.theme_data.get("taskbar_bg", "#333"), fg=self.theme_data.get("taskbar_fg", "white"), font=("Arial", 10))
self.clock_lbl.pack(side="right", padx=10)
self.update_clock()
# System Monitor
self.sysmon_canvas = tk.Canvas(self.taskbar, height=24, bg=self.theme_data.get("taskbar_bg", "#333"), highlightthickness=0)
self.sysmon_canvas.pack(side="right", padx=10, pady=3)
self.last_cpu_total = 0
self.last_cpu_idle = 0
self.update_sysmon()
# Keyboard Layout
self.kb_layout = "fr"
self.kb_btn = tk.Button(self.taskbar, text="FR", bg=self.theme_data.get("panel_btn_bg", "#555"), fg=self.theme_data.get("taskbar_fg", "white"), font=("Arial", 10, "bold"), command=self.toggle_keyboard)
self.kb_btn.pack(side="right", padx=5, pady=5)
self.apply_keyboard_layout()
# Wi-Fi Module
from core.wifi import WifiManager
self.wifi_manager = WifiManager(self)
self.sudo_pwd = None
self.wifi_btn = tk.Button(self.taskbar, image=self.icons.get("btn_wifi"), bg=self.theme_data.get("panel_btn_bg", "#555"), fg=self.theme_data.get("taskbar_fg", "white"), command=self.show_wifi_menu)
self.wifi_btn.pack(side="right", padx=5, pady=5)
ToolTip(self.wifi_btn, "Wi-Fi")
# USB Module
self.usb_btn = tk.Button(self.taskbar, image=self.icons.get("btn_usb"), bg=self.theme_data.get("panel_btn_bg", "#555"), fg=self.theme_data.get("taskbar_fg", "white"), command=self.show_usb_menu)
self.usb_btn.pack(side="right", padx=5, pady=5)
ToolTip(self.usb_btn, "Périphériques USB")
self.known_unmounted_usbs = set()
self.after(3000, self.update_usb)
# Audio Module
from core.audio import AudioManager
self.audio_manager = AudioManager(self)
self.audio_btn = tk.Button(self.taskbar, image=self.icons.get("btn_audio"), bg=self.theme_data.get("panel_btn_bg", "#555"), fg=self.theme_data.get("taskbar_fg", "white"), command=self.show_audio_menu)
self.audio_btn.pack(side="right", padx=5, pady=5)
ToolTip(self.audio_btn, "Contrôle du volume")
# Start Menu (hidden by default)
self.start_menu = tk.Frame(self.desktop_frame, bg="white", relief="raised", bd=2)
self.start_menu_visible = False
self.build_start_menu()
# Hide menu when clicking desktop
self.desktop_frame.bind("<ButtonPress-1>", self.hide_start_menu)
# Context menu (Right click)
self.context_menu = tk.Menu(self, tearoff=0)
self.context_menu.add_command(label="Paramètres", command=lambda: self.app_manager.launch_app({"name": "Paramètres", "module": "apps.parametres.app"}))
self.context_menu.add_separator()
self.context_menu.add_command(label="Redémarrer GrimOS", command=self.shutdown_grimos)
self.context_menu.add_separator()
self.context_menu.add_command(label="Annuler", command=lambda: None)
def show_context_menu(event):
try:
self.context_menu.tk_popup(event.x_root, event.y_root)
finally:
self.context_menu.grab_release()
self.desktop_frame.bind("<Button-3>", show_context_menu)
# Restore previous session if any
self.app_manager.restore_session()
# Start power management
self.after(5000, self.check_power)
# Start clipboard sync
self.last_prim = ""
self.after(1000, self.sync_clipboards)
# Start IPC notifications watcher
self.after(2000, self.check_notifications)
def check_notifications(self):
notify_file = "/tmp/grimos_notify"
if os.path.exists(notify_file):
try:
with open(notify_file, "r", encoding="utf-8") as f:
msg = f.read().strip()
os.remove(notify_file)
if msg:
if msg.startswith("POPUP:"):
from tkinter import messagebox
messagebox.showinfo("Message Système", msg[6:])
else:
self.show_toast(msg)
except Exception:
pass
self.after(1000, self.check_notifications)
def sync_clipboards(self):
try:
prim = self.selection_get(selection="PRIMARY")
if prim and prim != self.last_prim:
self.clipboard_clear()
self.clipboard_append(prim)
self.last_prim = prim
except:
pass
self.after(500, self.sync_clipboards)
def check_power(self):
try:
is_ac = True
if os.path.exists('/sys/class/power_supply/AC/online'):
with open('/sys/class/power_supply/AC/online', 'r') as f:
val = f.read().strip()
if val == '0':
is_ac = False
elif os.path.exists('/sys/class/power_supply/ACAD/online'):
with open('/sys/class/power_supply/ACAD/online', 'r') as f:
val = f.read().strip()
if val == '0':
is_ac = False
if is_ac:
subprocess.run(['xset', 's', 'off'], capture_output=True)
subprocess.run(['xset', '-dpms'], capture_output=True)
else:
res = subprocess.run(['xprintidle'], capture_output=True, text=True)
if res.returncode == 0:
idle_ms = int(res.stdout.strip())
if idle_ms > 180000: # 3 minutes
pwd = self.settings.get("sudo_pwd", "")
subprocess.run(['sudo', '-S', 'poweroff'], input=pwd+'\n', text=True, capture_output=True)
except Exception:
pass
self.after(10000, self.check_power)
def update_clock(self):
time_string = strftime('%H:%M')
self.clock_lbl.config(text=time_string)
self.after(1000, self.update_clock)
def update_sysmon(self):
try:
with open('/proc/meminfo', 'r') as f:
lines = f.readlines()
mem = {}
for line in lines:
parts = line.split()
if len(parts) >= 2:
mem[parts[0].strip(':')] = int(parts[1])
ram_pct = int(100 * (1 - mem.get('MemAvailable', mem.get('MemFree', 0)) / mem.get('MemTotal', 1)))
swap_tot = mem.get('SwapTotal', 0)
swap_pct = int(100 * (1 - mem.get('SwapFree', 0) / swap_tot)) if swap_tot > 0 else 0
with open('/proc/stat', 'r') as f:
cpu_line = f.readline().split()
cpu_idle = int(cpu_line[4]) + int(cpu_line[5])
cpu_total = sum(int(x) for x in cpu_line[1:8])
cpu_diff = cpu_total - self.last_cpu_total
idle_diff = cpu_idle - self.last_cpu_idle
cpu_pct = int(100 * (1 - idle_diff / cpu_diff)) if cpu_diff > 0 else 0
self.last_cpu_total = cpu_total
self.last_cpu_idle = cpu_idle
graph_width = self.settings.get("cpu_graph_width", 10)
if not hasattr(self, 'cpu_history'):
self.cpu_history = [0] * graph_width
self.ram_history = [0] * graph_width
self.swap_history = [0] * graph_width
while len(self.cpu_history) > graph_width:
self.cpu_history.pop(0)
self.ram_history.pop(0)
self.swap_history.pop(0)
while len(self.cpu_history) < graph_width:
self.cpu_history.insert(0, 0)
self.ram_history.insert(0, 0)
self.swap_history.insert(0, 0)
self.cpu_history.pop(0)
self.cpu_history.append(cpu_pct)
self.ram_history.pop(0)
self.ram_history.append(ram_pct)
self.swap_history.pop(0)
self.swap_history.append(swap_pct)
temp = 0
if os.path.exists('/sys/class/thermal/thermal_zone0/temp'):
with open('/sys/class/thermal/thermal_zone0/temp', 'r') as f:
temp = int(int(f.read().strip()) / 1000)
cap = None
status = ""
try:
for bat_dir in os.listdir('/sys/class/power_supply'):
if bat_dir.startswith('BAT') or bat_dir.startswith('macsmc-battery'):
with open(f'/sys/class/power_supply/{bat_dir}/capacity', 'r') as f:
cap = int(f.read().strip())
with open(f'/sys/class/power_supply/{bat_dir}/status', 'r') as f:
stat_txt = f.read().strip()
if stat_txt == "Charging": status = "⚡"
elif stat_txt == "Discharging": status = "🔋"
elif stat_txt == "Full": status = "🔌"
break
except Exception:
pass
self.sysmon_canvas.delete("all")
bar_w = 3
graph_px_width = graph_width * bar_w
graph_h = 20
pad_y = 2
current_x = 5
fg_color = self.theme_data.get("taskbar_fg", "white")
self.sysmon_canvas.create_rectangle(current_x, pad_y, current_x + graph_px_width, pad_y + graph_h, outline="black")
for i, val in enumerate(self.cpu_history):
h = int((val / 100) * graph_h)
if h > 0:
self.sysmon_canvas.create_rectangle(current_x + i * bar_w, pad_y + graph_h - h, current_x + (i + 1) * bar_w, pad_y + graph_h, fill="green", outline="")
current_x += graph_px_width + 5
self.sysmon_canvas.create_text(current_x, pad_y + graph_h//2, text=f"{temp:02d}°C", fill=fg_color, anchor="w", font=("Arial", 9))
current_x += 35
self.sysmon_canvas.create_rectangle(current_x, pad_y, current_x + graph_px_width, pad_y + graph_h, outline="black")
for i, val in enumerate(self.ram_history):
h = int((val / 100) * graph_h)
if h > 0:
self.sysmon_canvas.create_rectangle(current_x + i * bar_w, pad_y + graph_h - h, current_x + (i + 1) * bar_w, pad_y + graph_h, fill="blue", outline="")
current_x += graph_px_width + 5
self.sysmon_canvas.create_rectangle(current_x, pad_y, current_x + graph_px_width, pad_y + graph_h, outline="black")
for i, val in enumerate(self.swap_history):
h = int((val / 100) * graph_h)
if h > 0:
self.sysmon_canvas.create_rectangle(current_x + i * bar_w, pad_y + graph_h - h, current_x + (i + 1) * bar_w, pad_y + graph_h, fill="red", outline="")
current_x += graph_px_width + 10
if cap is not None:
bat_w = 20
bat_h = 10
bat_y = pad_y + (graph_h - bat_h) // 2
self.sysmon_canvas.create_rectangle(current_x, bat_y, current_x + bat_w, bat_y + bat_h, outline=fg_color)
self.sysmon_canvas.create_rectangle(current_x + bat_w, bat_y + 3, current_x + bat_w + 2, bat_y + bat_h - 3, fill=fg_color, outline="")
fill_w = int((cap / 100) * (bat_w - 2))
if fill_w > 0:
self.sysmon_canvas.create_rectangle(current_x + 1, bat_y + 1, current_x + 1 + fill_w, bat_y + bat_h - 1, fill="orange", outline="")
current_x += bat_w + 5
self.sysmon_canvas.create_text(current_x, pad_y + graph_h//2, text=f"{status} {cap}%", fill=fg_color, anchor="w", font=("Arial", 9))
current_x += 45
self.sysmon_canvas.config(width=current_x + 5)
except Exception:
pass
self.after(1000, self.update_sysmon)
def update_usb(self):
pwd = self.settings.get("sudo_pwd")
if pwd:
try:
res = subprocess.run(['lsblk', '-J', '-o', 'NAME,RM,MOUNTPOINT'], text=True, capture_output=True)
import json
data = json.loads(res.stdout)
current_unmounted = set()
for bd in data.get('blockdevices', []):
if bd.get('rm') in [True, '1', 1]:
children = bd.get('children', [])
if children:
for child in children:
if not child.get('mountpoint'):
current_unmounted.add(child.get('name'))
else:
if not bd.get('mountpoint'):
current_unmounted.add(bd.get('name'))
# Find new unmounted drives
new_drives = current_unmounted - self.known_unmounted_usbs
for d in new_drives:
mnt_dir = f"/tmp/usb_{d}"
os.makedirs(mnt_dir, exist_ok=True)
subprocess.run(['sudo', '-S', 'mount', f'/dev/{d}', mnt_dir], input=pwd+'\n', text=True, capture_output=True)
self.known_unmounted_usbs = current_unmounted
except:
pass
self.after(3000, self.update_usb)
def show_usb_menu(self):
pwd = self.settings.get("sudo_pwd")
if not pwd:
from tkinter import messagebox
messagebox.showerror("Erreur", "Mot de passe Sudo non configuré dans Paramètres.")
return
try:
res = subprocess.run(['lsblk', '-J', '-o', 'NAME,RM,MOUNTPOINT,SIZE'], text=True, capture_output=True)
import json
data = json.loads(res.stdout)
mounted_devices = []
for bd in data.get('blockdevices', []):
if bd.get('rm') in [True, '1', 1]:
children = bd.get('children', [])
if children:
for child in children:
if child.get('mountpoint'):
mounted_devices.append(child)
else:
if bd.get('mountpoint'):
mounted_devices.append(bd)
if not mounted_devices:
from tkinter import messagebox
messagebox.showinfo("USB", "Aucun périphérique USB monté actuellement.")
return
if hasattr(self, 'usb_popup') and self.usb_popup.winfo_exists():
self.usb_popup.destroy()
return
self.usb_popup = tk.Toplevel(self)
self.usb_popup.title("Éjecter USB")
self.usb_popup.geometry("300x200")
self.usb_popup.update_idletasks()
x = self.winfo_width() - 310
y = self.winfo_height() - 250
self.usb_popup.geometry(f"+{x}+{y}")
self.usb_popup.transient(self.winfo_toplevel())
tk.Label(self.usb_popup, text="Cliquez pour éjecter :", font=("Arial", 10, "bold")).pack(pady=5)
for dev in mounted_devices:
name = dev.get('name')
mountpoint = dev.get('mountpoint')
size = dev.get('size')
def unmount(n=name, m=mountpoint):
r = subprocess.run(['sudo', '-S', 'umount', f'/dev/{n}'], input=pwd+'\n', text=True, capture_output=True)
from tkinter import messagebox
if r.returncode == 0:
self.known_unmounted_usbs.add(n)
subprocess.run(['sudo', '-S', 'rmdir', m], input=pwd+'\n', text=True)
messagebox.showinfo("USB", f"Le périphérique {n} a été éjecté en toute sécurité.")
if self.usb_popup.winfo_exists():
self.usb_popup.destroy()
else:
messagebox.showerror("Erreur", r.stderr)
tk.Button(self.usb_popup, text=f"⏏️ /dev/{name} ({size})\n{mountpoint}", command=unmount).pack(fill="x", padx=5, pady=2)
except Exception as e:
from tkinter import messagebox
messagebox.showerror("Erreur", str(e))
def show_audio_menu(self):
if hasattr(self, 'audio_popup') and self.audio_popup.winfo_exists():
self.audio_popup.destroy()
return
self.audio_popup = tk.Toplevel(self)
self.audio_popup.overrideredirect(True)
self.audio_popup.configure(bg="#222", bd=1, relief="solid")
# Positionnement au-dessus du bouton
x = self.audio_btn.winfo_rootx()
y = self.audio_btn.winfo_rooty() - 230
self.audio_popup.geometry(f"120x220+{x-30}+{y}")
# Titre
tk.Label(self.audio_popup, text="Volume", font=("Arial", 10, "bold"), bg="#222", fg="white").pack(pady=5)
# Slider
current_vol = self.audio_manager.get_volume()
def on_volume_change(val):
self.audio_manager.set_volume(int(val))
vol_scale = tk.Scale(self.audio_popup, from_=100, to=0, orient="vertical", bg="#222", fg="white", highlightthickness=0, command=on_volume_change)
vol_scale.set(current_vol)
vol_scale.pack(fill="y", expand=True)
# Bouton de test
btn_test = tk.Button(self.audio_popup, text="Tester le son", bg="#4CAF50", fg="white", relief="flat", command=self.audio_manager.test_audio)
btn_test.pack(pady=5, padx=10, fill="x")
# Binding pour fermer quand on clique ailleurs
def check_focus(e):
if str(e.widget) not in str(self.audio_popup):
self.audio_popup.destroy()
# self.audio_popup.bind("<FocusOut>", lambda e: self.audio_popup.destroy())
self.audio_popup.focus_set()
def show_wifi_menu(self):
if not self.wifi_manager.iface:
from tkinter import messagebox
messagebox.showerror("Wi-Fi", "Aucune interface Wi-Fi détectée sur ce système.")
return
pwd = self.settings.get("sudo_pwd")
if not pwd:
from tkinter import messagebox
messagebox.showerror("Wi-Fi", "Veuillez configurer votre mot de passe système (Sudo) dans l'application Paramètres.")
return
if hasattr(self, 'wifi_popup') and self.wifi_popup.winfo_exists():
self.wifi_popup.destroy()
return
self.wifi_popup = tk.Toplevel(self)
self.wifi_popup.title("Réseaux Wi-Fi")
self.wifi_popup.geometry("300x450")
self.wifi_popup.update_idletasks()
x = self.winfo_width() - 310
y = self.winfo_height() - 500
self.wifi_popup.geometry(f"+{x}+{y}")
self.wifi_popup.transient(self.winfo_toplevel())
ssid_actuel, ip_actuelle = self.wifi_manager.get_current_status(pwd)
if ssid_actuel:
info_frame = tk.Frame(self.wifi_popup, bg="#e0f7fa", bd=1, relief="solid")
info_frame.pack(fill="x", padx=5, pady=5)
tk.Label(info_frame, text=f"Connecté : {ssid_actuel}", font=("Arial", 10, "bold"), bg="#e0f7fa", fg="#006064").pack()
if ip_actuelle:
tk.Label(info_frame, text=f"IP : {ip_actuelle}", font=("Arial", 9), bg="#e0f7fa", fg="#00838f").pack()
lbl = tk.Label(self.wifi_popup, text="Recherche de réseaux en cours...", font=("Arial", 10, "italic"))
lbl.pack(pady=10)
listbox = tk.Listbox(self.wifi_popup, font=("Arial", 10))
listbox.pack(fill="both", expand=True, padx=5, pady=5)
btn_connect = tk.Button(self.wifi_popup, text="Se connecter", state="disabled")
btn_connect.pack(fill="x", padx=5, pady=5)
networks_data = []
def on_select(event):
if listbox.curselection():
btn_connect.config(state="normal")
listbox.bind('<<ListboxSelect>>', on_select)
def do_connect():
sel = listbox.curselection()
if not sel: return
ssid = networks_data[sel[0]]['ssid']
psk = simpledialog.askstring("Wi-Fi", f"Clé de sécurité pour '{ssid}' (vide si réseau ouvert) :")
if psk is not None:
lbl.config(text="Connexion en cours...")
def connect_thread():
success, msg = self.wifi_manager.connect_network(pwd, ssid, psk)
def callback():
from tkinter import messagebox
if success:
messagebox.showinfo("Wi-Fi", f"Connecté à {ssid}.")
self.wifi_popup.destroy()
else:
messagebox.showerror("Erreur", msg)
lbl.config(text="Erreur.")
self.after(0, callback)
import threading
threading.Thread(target=connect_thread, daemon=True).start()
btn_connect.config(command=do_connect)
def scan_thread():
nets = self.wifi_manager.scan_networks(pwd)
def callback():
if not self.wifi_popup.winfo_exists(): return
lbl.config(text=f"{len(nets)} réseaux détectés.")
listbox.delete(0, tk.END)
networks_data.clear()
for net in nets:
networks_data.append(net)
listbox.insert(tk.END, f"{net['ssid']} ({net['signal']} dBm)")
self.after(0, callback)
import threading
threading.Thread(target=scan_thread, daemon=True).start()
def toggle_keyboard(self):
if self.kb_layout == "fr":
self.kb_layout = "us"
self.kb_btn.config(text="US")
else:
self.kb_layout = "fr"
self.kb_btn.config(text="FR")
self.apply_keyboard_layout()
def apply_keyboard_layout(self):
try:
subprocess.run(["setxkbmap", self.kb_layout])
except Exception as e:
print(f"Erreur clavier: {e}")
def build_start_menu(self):
for widget in self.start_menu.winfo_children():
widget.destroy()
tk.Label(self.start_menu, text="GrimOS", bg="lightgray", font=("Arial", 10, "bold")).pack(fill="x", pady=2)
self.start_menu_container = tk.Frame(self.start_menu, bg="white")
self.start_menu_container.pack(fill="both", expand=True)
self.start_menu_canvas = tk.Canvas(self.start_menu_container, bg="white", highlightthickness=0)
self.start_menu_scrollbar = tk.Scrollbar(self.start_menu_container, orient="vertical", command=self.start_menu_canvas.yview)
self.start_menu_content = tk.Frame(self.start_menu_canvas, bg="white")
self.start_menu_content.bind(
"<Configure>",
lambda e: self.start_menu_canvas.configure(scrollregion=self.start_menu_canvas.bbox("all"))
)
self.start_menu_canvas.create_window((0, 0), window=self.start_menu_content, anchor="nw", width=230)
self.start_menu_canvas.configure(yscrollcommand=self.start_menu_scrollbar.set)
def _on_mousewheel(event):
if self.start_menu_visible:
self.start_menu_canvas.yview_scroll(int(-1*(event.delta/120)), "units")
self.start_menu.bind_all("<MouseWheel>", _on_mousewheel)
self.start_menu.bind_all("<Button-4>", lambda e: self.start_menu_canvas.yview_scroll(-1, "units") if self.start_menu_visible else None)
self.start_menu.bind_all("<Button-5>", lambda e: self.start_menu_canvas.yview_scroll(1, "units") if self.start_menu_visible else None)
categories = {}
for app in self.apps:
cat = app.get("category", "Autres")
if cat not in categories:
categories[cat] = []
categories[cat].append(app)
for cat in sorted(categories.keys()):
tk.Label(self.start_menu_content, text=f"── {cat} ──", bg="white", fg="gray", font=("Arial", 9, "italic")).pack(fill="x", pady=(5,0))
for app in categories[cat]:
icon_img = self.app_icons.get(app.get("module"))
btn = tk.Button(self.start_menu_content, text=" " + app.get("name"), image=icon_img, compound="left", relief="flat", anchor="w",
command=lambda a=app: self.launch_from_menu(a))
btn.pack(fill="x", padx=10, pady=1)
tk.Frame(self.start_menu_content, height=2, bg="black").pack(fill="x", pady=2)
def generate_grimoire():
self.hide_start_menu()
import os
from tkinter import messagebox
grimoire_path = os.path.expanduser("~/Grimoire_GrimOS.md")
content = "# Le Grimoire GrimOS\n\n"
content += "Bienvenue dans GrimOS (Graphical Runtime Integrated Minimal Operating System).\n"
content += "Ce document est généré automatiquement et compile la documentation du système.\n\n"
content += "## 1. Créer et intégrer une application\n"
content += "Pour créer une application compatible GrimOS, créez un fichier `.py` contenant :\n"
content += "```python\nimport tkinter as tk\n"
content += "def start(window, app_manager=None, **kwargs):\n"
content += " # 'window' est un tk.Frame pré-créé par GrimOS.\n"
content += " tk.Label(window, text=\"Mon Appli\").pack()\n```\n\n"
content += "**Note de compatibilité :** Ne créez JAMAIS de `tk.Tk()` dans la fonction `start()`, cela casserait l'affichage sans gestionnaire de fenêtres.\n\n"
content += "Ensuite, pour ajouter l'application au Menu Démarrer, éditez le fichier `config/applications.json` dans le dossier de GrimOS :\n"
content += "```json\n{\n \"name\": \"Mon App\",\n \"module\": \"chemin.vers.le.module\",\n \"category\": \"Autres\"\n}\n```\n"
content += "Relancez la session, et votre application apparaîtra !\n\n"
content += "## 2. Aides des Applications Intégrées\n"
content += "### Éditeur de Code\n"
content += "Conçu pour le développement natif. Utilisez le bouton 'Nouveau GUI' pour générer un code complet (boilerplate) pour démarrer une application GrimOS, testable même en standalone via son propre bloc `__main__`.\n\n"
content += "### Explorateur\n"
content += "Double-cliquez pour naviguer. Clic-droit pour renommer ou supprimer. La case 'Fichiers cachés' permet d'afficher les éléments systèmes. Le bouton `>_ Terminal` permet d'ouvrir une invite de commande dans le dossier visité.\n\n"
content += "### Terminal\n"
content += "Environnement émulé. Commandes courantes supportées avec historique. La commande `sudo` est volontairement bloquée par sécurité (utilisez l'application Xterm pour l'administration système pure).\n"
try:
with open(grimoire_path, "w", encoding="utf-8") as f:
f.write(content)
self.app_manager.launch_app({"name": "Éditeur de Code", "module": "apps.editeur.app"}, filepath=grimoire_path)
except Exception as e:
messagebox.showerror("Erreur", f"Impossible de générer le Grimoire : {e}")
btn_help = tk.Button(self.start_menu_content, text=" Générer le Grimoire", image=self.icons.get('menu_grimoire'), compound="left", relief="flat", anchor="w", fg="blue", command=generate_grimoire)
btn_help.pack(fill="x", padx=5, pady=2)
tk.Frame(self.start_menu_content, height=1, bg="lightgray").pack(fill="x", pady=2)
btn_logoff = tk.Button(self.start_menu_content, text=" Fermer la session", image=self.icons.get('menu_logoff'), compound="left", relief="flat", anchor="w", fg="black", command=self.shutdown_grimos)
btn_logoff.pack(fill="x", padx=5, pady=2)
btn_reboot = tk.Button(self.start_menu_content, text=" Redémarrer", image=self.icons.get('menu_reboot'), compound="left", relief="flat", anchor="w", fg="orange", command=self.reboot_system)
btn_reboot.pack(fill="x", padx=5, pady=2)
btn_poweroff = tk.Button(self.start_menu_content, text=" Arrêter", image=self.icons.get('menu_poweroff'), compound="left", relief="flat", anchor="w", fg="red", command=self.poweroff_system)
btn_poweroff.pack(fill="x", padx=5, pady=2)
def toggle_start_menu(self):
if self.start_menu_visible:
self.hide_start_menu()
else:
self.start_menu_content.update_idletasks()
req_h = self.start_menu_content.winfo_reqheight() + 30 # Label de titre et bordures
max_h = self.desktop_frame.winfo_height() - 20
if req_h > max_h:
self.start_menu_scrollbar.pack(side="right", fill="y")
self.start_menu_canvas.pack(side="left", fill="both", expand=True)
final_h = max_h
else:
self.start_menu_scrollbar.pack_forget()
self.start_menu_canvas.pack(side="left", fill="both", expand=True)
final_h = req_h
self.start_menu.place(x=5, y=self.desktop_frame.winfo_height() - final_h - 5, height=final_h, width=250)
self.start_menu.lift()
self.start_menu_visible = True
def hide_start_menu(self, event=None):
if self.start_menu_visible:
self.start_menu.place_forget()
self.start_menu_visible = False
def launch_from_menu(self, app):
self.hide_start_menu()
self.app_manager.launch_app(app)
def register_window(self, win):
if win not in self.taskbar_btns:
icon_img = self.app_icons.get(win.app_config.get("module"))
tooltip_text = win.filepath if hasattr(win, "filepath") and win.filepath else win.title
btn = tk.Button(self.taskbar_windows, image=icon_img, bg="#444", fg="white",
command=lambda: self.toggle_window(win))
btn.pack(side="left", padx=2, pady=5)
self.taskbar_btns[win] = btn
ToolTip(btn, tooltip_text)
def update_taskbar_btn(self, win):
if win in self.taskbar_btns:
if not win.winfo_ismapped():
# Fenêtre minimisée -> fond plus sombre ou différent pour l'indiquer
self.taskbar_btns[win].config(bg="#888", relief="sunken")
else:
self.taskbar_btns[win].config(bg="#444", relief="raised")
def toggle_window(self, win):
if win.winfo_ismapped():
win.minimize()
else:
win.restore()
def remove_window(self, win):
if win in self.taskbar_btns:
self.taskbar_btns[win].destroy()
del self.taskbar_btns[win]
if win in self.app_manager.active_windows:
self.app_manager.active_windows.remove(win)
def save_and_quit(self):
self.app_manager.save_session()
self.parent.destroy()
def shutdown_grimos(self):
self.save_and_quit()
def reboot_system(self):
from tkinter import messagebox
pwd = self.settings.get("sudo_pwd")
if not pwd:
messagebox.showerror("Erreur", "Veuillez configurer votre mot de passe système (Sudo) dans les Paramètres.")
return
if pwd is not None:
self.app_manager.save_session()
try:
res = subprocess.run(['sudo', '-S', 'systemctl', 'reboot'], input=pwd + '\n', text=True, capture_output=True)
if res.returncode != 0:
messagebox.showerror("Erreur", f"Échec de la commande:\n{res.stderr}")
except Exception as e:
print(f"Erreur reboot: {e}")
def poweroff_system(self):
from tkinter import messagebox
pwd = self.settings.get("sudo_pwd")
if not pwd:
messagebox.showerror("Erreur", "Veuillez configurer votre mot de passe système (Sudo) dans les Paramètres.")
return
if pwd is not None:
self.app_manager.save_session()
try:
res = subprocess.run(['sudo', '-S', 'systemctl', 'poweroff'], input=pwd + '\n', text=True, capture_output=True)
if res.returncode != 0:
messagebox.showerror("Erreur", f"Échec de la commande:\n{res.stderr}")
except Exception as e:
print(f"Erreur poweroff: {e}")
def show_toast(self, message):
toast = tk.Label(self.desktop_frame, text=message, bg="#333", fg="white", font=("Arial", 10, "bold"), padx=15, pady=10, relief="solid", bd=1)
y_pos = -10
for t in self.toasts:
self.desktop_frame.update_idletasks()
y_pos -= t.winfo_reqheight() + 5
toast.place(relx=1.0, rely=1.0, x=-10, y=y_pos, anchor="se")
self.toasts.append(toast)
def remove_toast():
if toast in self.toasts:
self.toasts.remove(toast)
toast.destroy()
current_y = -10
for t in self.toasts:
self.desktop_frame.update_idletasks()
t.place(relx=1.0, rely=1.0, x=-10, y=current_y, anchor="se")
current_y -= t.winfo_reqheight() + 5
self.after(4000, remove_toast)
def load_desktop_icons(self):
import json
desktop_config_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "config", "desktop.json")
icon_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "icons")
icon_theme = self.theme_data.get("icon_theme", "grimos")
icon_theme_dir = os.path.join(icon_dir, "themes", icon_theme)
if not os.path.exists(desktop_config_path):
try:
with open(desktop_config_path, "w") as f:
json.dump([], f)
except:
pass
for w in self.desktop_frame.winfo_children():
if getattr(w, "_is_desktop_icon", False):
w.destroy()
try:
with open(desktop_config_path, "r") as f:
icons = json.load(f)
for icon_data in icons:
name = icon_data.get("name", "Fichier")
path = icon_data.get("path", "")
x = icon_data.get("x", 50)
y = icon_data.get("y", 50)
img_name = "blocnotes.png"
if os.path.isdir(path):
img_name = "explorateur.png"
elif path.endswith(".py"):
img_name = "terminal.png"
img_path = os.path.join(icon_theme_dir, img_name)
if not os.path.exists(img_path):
img_path = os.path.join(icon_dir, img_name)
try:
photo = tk.PhotoImage(file=img_path)
except:
photo = None
# Truncate long names
disp_name = name if len(name) < 15 else name[:12]+"..."
# Use theme colors for icon label
fg_color = self.theme_data.get("desktop_fg", "white")
btn = tk.Button(self.desktop_frame, text=disp_name, image=photo, compound="top", relief="flat", bg=self.desktop_frame.cget("bg"), fg=fg_color, justify="center", width=80, command=lambda p=path: self.launch_file(p))
btn.image = photo # Keep reference
btn._is_desktop_icon = True
btn.place(x=x, y=y)
def make_draggable_and_menu(widget, data, icon_list):
def start_drag(event):
widget._drag_start_x = event.x
widget._drag_start_y = event.y
def do_drag(event):
nx = widget.winfo_x() - widget._drag_start_x + event.x
ny = widget.winfo_y() - widget._drag_start_y + event.y
widget.place(x=nx, y=ny)
def stop_drag(event):
# Don't register drag if the widget was just clicked
if getattr(widget, "_is_menu_open", False): return
data["x"] = widget.winfo_x()
data["y"] = widget.winfo_y()
self.save_desktop_icons(icon_list)
def show_menu(event):
menu = tk.Menu(self.desktop_frame, tearoff=0)
def delete_shortcut():
if data in icon_list:
icon_list.remove(data)
self.save_desktop_icons(icon_list)
widget.destroy()
self.show_toast(f"Raccourci supprimé")
menu.add_command(label=" Supprimer le raccourci", image=self.icons.get('btn_trash'), compound="left", command=delete_shortcut)
widget._is_menu_open = True
try:
menu.tk_popup(event.x_root, event.y_root)
finally:
menu.grab_release()
widget._is_menu_open = False
widget.bind("<Button-1>", start_drag)
widget.bind("<B1-Motion>", do_drag)
widget.bind("<ButtonRelease-1>", stop_drag)
widget.bind("<Button-3>", show_menu)
make_draggable_and_menu(btn, icon_data, icons)
except Exception as e:
print("Erreur desktop icons:", e)
def save_desktop_icons(self, icons):
import json
desktop_config_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "config", "desktop.json")
try:
with open(desktop_config_path, "w") as f:
json.dump(icons, f, indent=4)
except:
pass
def launch_file(self, path):
if not os.path.exists(path):
self.show_toast(f"Fichier introuvable:\n{path}")
return
if os.path.isdir(path):
self.app_manager.launch_app({"name": "Explorateur", "module": "apps.explorateur.app"}, filepath=path)
elif path.endswith(".py"):
self.app_manager.launch_app({"name": "Éditeur de Code", "module": "apps.editeur.app"}, filepath=path)
elif path.endswith((".png", ".jpg", ".gif")):
self.app_manager.launch_app({"name": "Visionneuse", "module": "apps.visionneuse.app"}, filepath=path)
else:
self.app_manager.launch_app({"name": "Bloc-notes", "module": "apps.blocnotes.app"}, filepath=path)