765 lines
24 KiB
Python
Executable File
765 lines
24 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Dashboard Launcher - Application GTK3 + WebKit2
|
|
Affiche une interface web pour lancer des connexions SSH et autres services.
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import yaml
|
|
import gi
|
|
import subprocess
|
|
import re
|
|
import threading
|
|
|
|
gi.require_version('Gtk', '3.0')
|
|
gi.require_version('WebKit2', '4.1')
|
|
gi.require_version('Gdk', '3.0')
|
|
|
|
from gi.repository import Gtk, WebKit2, Gio, Gdk, GLib
|
|
import cairo
|
|
|
|
PID_FILE = '/tmp/dashboard-launcher.pid'
|
|
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
PROJECT_DIR = os.path.dirname(SCRIPT_DIR)
|
|
CONFIG_FILE = os.path.join(PROJECT_DIR, 'config', 'equipements.yaml')
|
|
ICONS_DIR = os.path.join(PROJECT_DIR, 'icons')
|
|
|
|
# Cache pour les résultats de ping
|
|
ping_results = {}
|
|
|
|
|
|
def ping_host(ip, timeout=1):
|
|
"""Ping une IP et retourne True si accessible."""
|
|
try:
|
|
# Nettoyer l'IP (enlever les doubles points, etc.)
|
|
clean_ip = ip.replace('..', '.')
|
|
result = subprocess.run(
|
|
['ping', '-c', '1', '-W', str(timeout), clean_ip],
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL,
|
|
timeout=timeout + 1
|
|
)
|
|
return result.returncode == 0
|
|
except Exception:
|
|
return False
|
|
|
|
|
|
def ping_all_hosts(hosts, callback):
|
|
"""Ping toutes les IPs en parallèle et appelle callback avec les résultats."""
|
|
def ping_thread(ip):
|
|
result = ping_host(ip)
|
|
ping_results[ip] = result
|
|
|
|
threads = []
|
|
for ip in hosts:
|
|
t = threading.Thread(target=ping_thread, args=(ip,))
|
|
t.daemon = True
|
|
t.start()
|
|
threads.append(t)
|
|
|
|
def wait_and_callback():
|
|
for t in threads:
|
|
t.join(timeout=2)
|
|
GLib.idle_add(callback)
|
|
|
|
threading.Thread(target=wait_and_callback, daemon=True).start()
|
|
|
|
|
|
def load_config():
|
|
"""Charge la configuration depuis le fichier YAML."""
|
|
try:
|
|
with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
|
|
return yaml.safe_load(f) or {}
|
|
except (FileNotFoundError, yaml.YAMLError) as e:
|
|
print(f"Erreur config: {e}")
|
|
return {}
|
|
|
|
|
|
def get_icon_html(icon_name, size=32):
|
|
"""Retourne le HTML pour une icône."""
|
|
if not icon_name:
|
|
return f'<span class="service-icon" style="font-size:{size}px">🔗</span>'
|
|
|
|
# Chemin vers image dans icons/
|
|
if icon_name.startswith('icons/'):
|
|
icon_path = os.path.join(PROJECT_DIR, icon_name)
|
|
if os.path.exists(icon_path):
|
|
return f'<img src="file://{icon_path}" class="service-img" style="width:{size}px;height:{size}px" alt="">'
|
|
|
|
# Emojis de fallback
|
|
icons = {
|
|
'utilities-terminal': '💻', 'terminal': '💻', 'web-browser': '🌐',
|
|
'applications-system': '⚙️', 'server': '🖥️', 'network-server': '🖧',
|
|
'preferences-desktop-remote-desktop': '🖥️', 'security-high': '🔒',
|
|
'go-home': '🏠', 'system-file-manager': '📁', 'firefox': '🦊',
|
|
'google-chrome': '🌐', 'code': '📝',
|
|
}
|
|
emoji = icons.get(icon_name, icon_name if len(icon_name) <= 2 else '🔗')
|
|
return f'<span class="service-icon" style="font-size:{size}px">{emoji}</span>'
|
|
|
|
|
|
def generate_html(config):
|
|
"""Génère le HTML avec layout: local en haut, distant à gauche, url à droite."""
|
|
|
|
apparence = config.get('apparence', {})
|
|
fenetre = config.get('fenetre', {})
|
|
theme = apparence.get('theme', 'dark')
|
|
font_size = apparence.get('police_taille', 14)
|
|
icon_distant = apparence.get('icon_taille_distant', 48)
|
|
icon_local = apparence.get('icon_taille_local', 64)
|
|
icon_url = apparence.get('icon_taille_url', 64)
|
|
icon_minitools = apparence.get('icon_taille_minitools', 32)
|
|
show_local_labels = apparence.get('afficher_label_local', True)
|
|
border_radius = apparence.get('border_radius', 12)
|
|
espacement_local = apparence.get('espacement_local', 10)
|
|
espacement_distant = apparence.get('espacement_distant', 6)
|
|
espacement_url = apparence.get('espacement_url', 8)
|
|
espacement_minitools = apparence.get('espacement_minitools', 8)
|
|
icon_fermer = apparence.get('icon_taille_fermer', 22)
|
|
icon_parametre = apparence.get('icon_taille_parametre', 22)
|
|
icon_theme_width = apparence.get('icon_taille_theme', 60)
|
|
icon_theme_height = int(icon_theme_width * 238 / 512) # Ratio de l'image night-day.png
|
|
border_radius_local = apparence.get('border_radius_local', 12)
|
|
|
|
# Colonnes configurables
|
|
col_url = fenetre.get('section_url', {}).get('colonne', 2)
|
|
|
|
distant = config.get('distant', [])
|
|
local = config.get('local', [])
|
|
urls = config.get('url', [])
|
|
|
|
# Section DISTANT avec status ping
|
|
distant_html = ""
|
|
for machine in distant:
|
|
ip = machine.get('ip', '')
|
|
nom = machine.get('nom', '')
|
|
# Vérifier le statut ping (vert si up, rouge si down, gris si inconnu)
|
|
is_up = ping_results.get(ip)
|
|
if is_up is None:
|
|
status_class = "status-unknown"
|
|
elif is_up:
|
|
status_class = "status-up"
|
|
else:
|
|
status_class = "status-down"
|
|
|
|
services_html = ""
|
|
for service in machine.get('services', []):
|
|
icon_html = get_icon_html(service.get('icon', ''), icon_distant)
|
|
services_html += f'''
|
|
<a href="{service.get('url', '')}" class="item" title="{nom} - {service.get('nom', '')}">
|
|
{icon_html}
|
|
<span class="label">{service.get('nom', '')}</span>
|
|
</a>'''
|
|
distant_html += f'''
|
|
<div class="ip-box">
|
|
<div class="ip-header"><span class="status-dot {status_class}" data-ip="{ip}"></span>{nom}</div>
|
|
<div class="ip-name">{ip}</div>
|
|
<div class="items">{services_html}</div>
|
|
</div>'''
|
|
|
|
# Section LOCAL
|
|
local_html = ""
|
|
for app in local:
|
|
icon_html = get_icon_html(app.get('icon', ''), icon_local)
|
|
label_html = f'<span class="label">{app.get("nom", "")}</span>' if show_local_labels else ''
|
|
local_html += f'''
|
|
<a href="app://run/{app.get('command', '')}" class="item item-local" title="{app.get('nom', '')}">
|
|
{icon_html}
|
|
{label_html}
|
|
</a>'''
|
|
|
|
minitools = config.get('minitools', [])
|
|
minitools_html = ""
|
|
for tool in minitools:
|
|
icon_html = get_icon_html(tool.get('icon', ''), icon_minitools)
|
|
label_html = f'<span class="label">{tool.get("nom", "")}</span>' if show_local_labels else ''
|
|
href = ''
|
|
if tool.get('command'):
|
|
href = f"app://run/{tool.get('command')}"
|
|
elif tool.get('url'):
|
|
href = tool.get('url')
|
|
if not href:
|
|
continue
|
|
minitools_html += f'''
|
|
<a href="{href}" class="item mini-item" title="{tool.get('nom', '')}">
|
|
{icon_html}
|
|
{label_html}
|
|
</a>'''
|
|
|
|
# Section URL
|
|
url_html = ""
|
|
for bookmark in urls:
|
|
icon_html = get_icon_html(bookmark.get('icon', ''), icon_url)
|
|
url_html += f'''
|
|
<a href="{bookmark.get('url', '')}" class="item item-url" title="{bookmark.get('nom', '')}">
|
|
{icon_html}
|
|
<span class="label">{bookmark.get('nom', '')}</span>
|
|
</a>'''
|
|
|
|
# Couleurs thème Adwaita - box = fond équipement (clair), item = fond service (foncé)
|
|
if theme == 'light':
|
|
c = {'bg': '#fafafa', 'container': '#ffffff', 'header': '#ebebeb', 'border': '#d0d0d0',
|
|
'text': '#2e2e2e', 'text2': '#5e5e5e', 'box': '#e8e8e8', 'item': '#f5f5f5',
|
|
'hover': '#888b8f', 'status': '#2ec27e'}
|
|
else:
|
|
c = {'bg': '#1e1e1e', 'container': '#242424', 'header': '#303030', 'border': '#1a1a1a',
|
|
'text': '#ffffff', 'text2': '#999999', 'box': '#3d3d3d', 'item': '#2d2d2d',
|
|
'hover': '#888b8f', 'status': '#2ec27e'}
|
|
|
|
# Surcharge avec les couleurs personnalisées si définies
|
|
if apparence.get('couleur_fond'):
|
|
c['container'] = apparence['couleur_fond']
|
|
if apparence.get('couleur_header'):
|
|
c['header'] = apparence['couleur_header']
|
|
else:
|
|
c['header'] = c['container'] # Par défaut, même couleur que le fond
|
|
if apparence.get('couleur_item'):
|
|
c['item'] = apparence['couleur_item']
|
|
if apparence.get('couleur_bordure'):
|
|
c['border'] = apparence['couleur_bordure']
|
|
if apparence.get('couleur_hover'):
|
|
c['hover'] = apparence['couleur_hover']
|
|
|
|
# Couleurs séparées pour chaque section (local, distant, url)
|
|
c['box_local'] = apparence.get('couleur_box_local') or c['box']
|
|
c['box_distant'] = apparence.get('couleur_box_distant') or c['box']
|
|
c['box_url'] = apparence.get('couleur_box_url') or c['box']
|
|
c['box_minitools'] = apparence.get('couleur_box_minitools') or c['box_local']
|
|
|
|
# Image night-day: 512x238 (ratio 2.15:1) - affichée entièrement comme toggle
|
|
|
|
html = f'''<!DOCTYPE html>
|
|
<html><head>
|
|
<meta charset="UTF-8">
|
|
<style>
|
|
* {{ box-sizing: border-box; margin: 0; padding: 0; }}
|
|
html, body {{ height: 100%; overflow: hidden; background: transparent; }}
|
|
body {{
|
|
font-family: 'Cantarell', 'Segoe UI', sans-serif;
|
|
font-size: {font_size}px;
|
|
font-weight: 400;
|
|
background: transparent;
|
|
color: {c['text']};
|
|
}}
|
|
.container {{
|
|
height: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
background: {c['container']};
|
|
border-radius: {border_radius}px;
|
|
border: 1px solid {c['border']};
|
|
overflow: hidden;
|
|
}}
|
|
/* Header avec LOCAL intégré */
|
|
.header {{
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: flex-start;
|
|
padding: 8px 14px;
|
|
background: {c['header']};
|
|
min-height: 80px;
|
|
gap: 12px;
|
|
}}
|
|
.header-local {{
|
|
display: flex;
|
|
align-items: center;
|
|
gap: {espacement_local}px;
|
|
flex: 1;
|
|
padding: 8px 12px;
|
|
background: {c['box_local']};
|
|
border-radius: {border_radius_local}px;
|
|
}}
|
|
.header-controls {{
|
|
display: flex;
|
|
gap: 4px;
|
|
align-items: center;
|
|
flex-shrink: 0;
|
|
}}
|
|
.header-btn {{
|
|
width: 32px;
|
|
height: 32px;
|
|
background: transparent;
|
|
border: none;
|
|
cursor: pointer;
|
|
border-radius: 6px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
transition: background 0.15s;
|
|
}}
|
|
.header-btn:hover {{
|
|
background: {c['hover']};
|
|
}}
|
|
.header-btn.btn-parametre img {{
|
|
width: {icon_parametre}px;
|
|
height: {icon_parametre}px;
|
|
object-fit: contain;
|
|
}}
|
|
.header-btn.btn-fermer img {{
|
|
width: {icon_fermer}px;
|
|
height: {icon_fermer}px;
|
|
object-fit: contain;
|
|
}}
|
|
.theme-btn {{
|
|
overflow: hidden;
|
|
width: {icon_theme_width}px;
|
|
height: {icon_theme_height}px;
|
|
padding: 0;
|
|
}}
|
|
.theme-btn img {{
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: contain;
|
|
}}
|
|
.main {{
|
|
flex: 1;
|
|
display: grid;
|
|
grid-template-columns: 1fr auto;
|
|
gap: 10px;
|
|
padding: 12px;
|
|
overflow: hidden;
|
|
}}
|
|
/* DISTANT à gauche - flex wrap pour ajuster la taille des boxes */
|
|
.section-distant {{
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: {espacement_distant}px;
|
|
align-content: start;
|
|
overflow-y: auto;
|
|
padding-right: 6px;
|
|
}}
|
|
/* URL à droite */
|
|
.section-url {{
|
|
display: grid;
|
|
grid-template-columns: repeat({col_url}, 1fr);
|
|
gap: {espacement_url}px;
|
|
padding: 12px;
|
|
background: {c['box_url']};
|
|
border-radius: 10px;
|
|
align-content: start;
|
|
min-width: 180px;
|
|
}}
|
|
/* Barre de mini outils */
|
|
.section-minitools {{
|
|
display: flex;
|
|
gap: {espacement_minitools}px;
|
|
padding: 10px 14px;
|
|
background: {c['box_minitools']};
|
|
border-top: 1px solid {c['border']};
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
overflow-x: auto;
|
|
}}
|
|
.section-minitools .mini-item {{
|
|
flex-direction: row;
|
|
padding: 6px 10px;
|
|
min-width: 90px;
|
|
}}
|
|
.section-minitools .mini-item .label {{
|
|
font-size: {font_size - 2}px;
|
|
}}
|
|
/* IP box - taille ajustée au contenu */
|
|
.ip-box {{
|
|
background: {c['box_distant']};
|
|
border-radius: 8px;
|
|
padding: 10px;
|
|
flex-shrink: 0;
|
|
}}
|
|
.ip-header {{
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
font-size: {font_size}px;
|
|
font-weight: 500;
|
|
color: {c['text']};
|
|
margin-bottom: 2px;
|
|
}}
|
|
.status-dot {{
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 50%;
|
|
flex-shrink: 0;
|
|
}}
|
|
.status-up {{
|
|
background: #2ec27e;
|
|
}}
|
|
.status-down {{
|
|
background: #e01b24;
|
|
}}
|
|
.status-unknown {{
|
|
background: #888888;
|
|
}}
|
|
.ip-name {{
|
|
font-size: {font_size - 2}px;
|
|
font-family: 'JetBrains Mono', 'Consolas', monospace;
|
|
color: {c['text2']};
|
|
margin-bottom: 8px;
|
|
margin-left: 14px;
|
|
}}
|
|
.items {{
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: {espacement_distant}px;
|
|
}}
|
|
.item {{
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 4px;
|
|
padding: 8px;
|
|
background: {c['item']};
|
|
border-radius: 8px;
|
|
text-decoration: none;
|
|
color: {c['text']};
|
|
cursor: pointer;
|
|
transition: background 0.15s, transform 0.1s;
|
|
}}
|
|
.item:hover {{
|
|
background: {c['hover']};
|
|
transform: translateY(-2px);
|
|
color: #fff;
|
|
}}
|
|
.item-local {{
|
|
background: transparent;
|
|
padding: 8px 12px;
|
|
}}
|
|
.item-local:hover {{
|
|
background: {c['item']};
|
|
}}
|
|
.item-url {{
|
|
background: transparent;
|
|
padding: 10px;
|
|
}}
|
|
.item-url:hover {{
|
|
background: {c['item']};
|
|
}}
|
|
.service-icon {{
|
|
line-height: 1;
|
|
}}
|
|
.service-img {{
|
|
object-fit: contain;
|
|
}}
|
|
.label {{
|
|
font-size: {font_size - 2}px;
|
|
font-weight: 400;
|
|
text-align: center;
|
|
color: {c['text2']};
|
|
max-width: 70px;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}}
|
|
.item:hover .label {{
|
|
color: #fff;
|
|
}}
|
|
/* Scrollbar style */
|
|
.section-distant::-webkit-scrollbar {{
|
|
width: 6px;
|
|
}}
|
|
.section-distant::-webkit-scrollbar-track {{
|
|
background: transparent;
|
|
}}
|
|
.section-distant::-webkit-scrollbar-thumb {{
|
|
background: {c['border']};
|
|
border-radius: 3px;
|
|
}}
|
|
.section-distant::-webkit-scrollbar-thumb:hover {{
|
|
background: {c['text2']};
|
|
}}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="header">
|
|
<div class="header-local">{local_html}</div>
|
|
<div class="header-controls">
|
|
<button class="header-btn theme-btn" onclick="location.href='app://toggle-theme'" title="Thème">
|
|
<img src="file://{ICONS_DIR}/night-day.png" alt="">
|
|
</button>
|
|
<button class="header-btn btn-parametre" onclick="location.href='app://settings'" title="Paramètres">
|
|
<img src="file://{ICONS_DIR}/parametre.png" alt="">
|
|
</button>
|
|
<button class="header-btn btn-fermer" onclick="location.href='app://close'" title="Fermer">
|
|
<img src="file://{ICONS_DIR}/fermer.png" alt="">
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="main">
|
|
<div class="section-distant">{distant_html}</div>
|
|
<div class="section-url">{url_html}</div>
|
|
</div>
|
|
<div class="section-minitools">{minitools_html}</div>
|
|
</div>
|
|
</body></html>'''
|
|
return html
|
|
|
|
|
|
class DashboardLauncherWindow(Gtk.Window):
|
|
def __init__(self):
|
|
super().__init__(title="Dashboard Launcher")
|
|
self.config = load_config()
|
|
self.fenetre_config = self.config.get('fenetre', {})
|
|
|
|
# Activer la transparence pour éviter le flash blanc
|
|
screen = self.get_screen()
|
|
visual = screen.get_rgba_visual()
|
|
if visual:
|
|
self.set_visual(visual)
|
|
self.set_app_paintable(True)
|
|
self.connect('draw', self.on_draw)
|
|
|
|
self.set_type_hint(Gdk.WindowTypeHint.POPUP_MENU)
|
|
self.set_skip_taskbar_hint(True)
|
|
self.set_skip_pager_hint(True)
|
|
self.set_decorated(False) # Pas de décoration pour éviter le flash
|
|
self.set_resizable(False)
|
|
|
|
if self.fenetre_config.get('toujours_au_dessus', False):
|
|
self.set_keep_above(True)
|
|
|
|
largeur = self.fenetre_config.get('largeur', 800)
|
|
hauteur = self.fenetre_config.get('hauteur', 450)
|
|
self.set_default_size(largeur, hauteur)
|
|
|
|
display = Gdk.Display.get_default()
|
|
ecran_num = self.fenetre_config.get('ecran', -1)
|
|
|
|
if ecran_num is None or ecran_num < 0:
|
|
seat = display.get_default_seat()
|
|
pointer = seat.get_pointer()
|
|
_, mouse_x, mouse_y = pointer.get_position()
|
|
monitor = display.get_monitor_at_point(mouse_x, mouse_y)
|
|
else:
|
|
n_monitors = display.get_n_monitors()
|
|
monitor = display.get_monitor(ecran_num) if ecran_num < n_monitors else display.get_primary_monitor() or display.get_monitor(0)
|
|
|
|
geom = monitor.get_geometry()
|
|
centrer = self.fenetre_config.get('centrer', True)
|
|
x = self.fenetre_config.get('x', 0)
|
|
y = self.fenetre_config.get('y', 50)
|
|
|
|
if centrer:
|
|
self.move(geom.x + (geom.width - largeur) // 2, geom.y + y)
|
|
else:
|
|
self.move(geom.x + x, geom.y + y)
|
|
|
|
self.connect('key-press-event', self.on_key_press)
|
|
|
|
settings = WebKit2.Settings()
|
|
settings.set_property('hardware-acceleration-policy', WebKit2.HardwareAccelerationPolicy.NEVER)
|
|
|
|
self.webview = WebKit2.WebView()
|
|
self.webview.set_settings(settings)
|
|
self.webview.connect('decide-policy', self.on_decide_policy)
|
|
|
|
# Fond transparent pour éviter le flash blanc
|
|
bg_color = Gdk.RGBA()
|
|
bg_color.red = 0
|
|
bg_color.green = 0
|
|
bg_color.blue = 0
|
|
bg_color.alpha = 0 # Transparent
|
|
self.webview.set_background_color(bg_color)
|
|
|
|
self.reload_content()
|
|
self.add(self.webview)
|
|
self.connect('destroy', self.on_destroy)
|
|
|
|
# Lancer le ping en arrière-plan
|
|
self.ping_timer_id = None
|
|
self.autohide_timer_id = None
|
|
self.mouse_inside = True
|
|
self.start_ping_check()
|
|
|
|
# Configurer le ping périodique
|
|
ping_interval = self.config.get('apparence', {}).get('ping_intervalle', 360)
|
|
if ping_interval > 0:
|
|
self.ping_timer_id = GLib.timeout_add_seconds(ping_interval, self.on_ping_timer)
|
|
|
|
# Fermeture sur perte de focus (clic extérieur)
|
|
if self.fenetre_config.get('fermer_sur_clic_exterieur', True):
|
|
self.connect('focus-out-event', self.on_focus_out)
|
|
|
|
# Autohide: fermer si souris hors fenêtre pendant X secondes
|
|
autohide_delay = self.fenetre_config.get('autohide', 0)
|
|
if autohide_delay > 0:
|
|
self.autohide_delay = autohide_delay
|
|
self.connect('enter-notify-event', self.on_mouse_enter)
|
|
self.connect('leave-notify-event', self.on_mouse_leave)
|
|
|
|
def on_draw(self, widget, cr):
|
|
"""Dessine un fond transparent."""
|
|
cr.set_source_rgba(0, 0, 0, 0)
|
|
cr.set_operator(cairo.OPERATOR_SOURCE)
|
|
cr.paint()
|
|
return False
|
|
|
|
def reload_content(self):
|
|
self.config = load_config()
|
|
self.webview.load_html(generate_html(self.config), 'file://')
|
|
|
|
def start_ping_check(self):
|
|
"""Lance le ping de toutes les IPs en arrière-plan."""
|
|
distant = self.config.get('distant', [])
|
|
hosts = [m.get('ip', '') for m in distant if m.get('ip')]
|
|
ping_all_hosts(hosts, self.on_ping_complete)
|
|
|
|
def on_ping_complete(self):
|
|
"""Callback appelé quand tous les pings sont terminés."""
|
|
self.reload_content()
|
|
|
|
def on_ping_timer(self):
|
|
"""Timer pour le ping périodique."""
|
|
self.start_ping_check()
|
|
return True # Continuer le timer
|
|
|
|
def on_destroy(self, widget):
|
|
"""Nettoyage à la fermeture."""
|
|
if self.ping_timer_id:
|
|
GLib.source_remove(self.ping_timer_id)
|
|
if self.autohide_timer_id:
|
|
GLib.source_remove(self.autohide_timer_id)
|
|
Gtk.main_quit()
|
|
|
|
def on_focus_out(self, widget, event):
|
|
"""Ferme la fenêtre quand elle perd le focus."""
|
|
self.destroy()
|
|
return True
|
|
|
|
def on_mouse_enter(self, widget, event):
|
|
"""Souris entre dans la fenêtre - annuler le timer autohide."""
|
|
self.mouse_inside = True
|
|
if self.autohide_timer_id:
|
|
GLib.source_remove(self.autohide_timer_id)
|
|
self.autohide_timer_id = None
|
|
return False
|
|
|
|
def on_mouse_leave(self, widget, event):
|
|
"""Souris quitte la fenêtre - démarrer le timer autohide."""
|
|
self.mouse_inside = False
|
|
if self.autohide_timer_id:
|
|
GLib.source_remove(self.autohide_timer_id)
|
|
self.autohide_timer_id = GLib.timeout_add_seconds(
|
|
self.autohide_delay, self.on_autohide_timeout
|
|
)
|
|
return False
|
|
|
|
def on_autohide_timeout(self):
|
|
"""Timer autohide expiré - fermer si souris toujours hors fenêtre."""
|
|
if not self.mouse_inside:
|
|
self.destroy()
|
|
return False # Ne pas répéter
|
|
|
|
def toggle_theme(self):
|
|
current = self.config.get('apparence', {}).get('theme', 'dark')
|
|
new_theme = 'light' if current == 'dark' else 'dark'
|
|
try:
|
|
with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
|
|
content = f.read()
|
|
# Regex précise: uniquement "theme:" en début de ligne (après espaces)
|
|
content = re.sub(r'^(\s*)theme:\s*"?\w+"?', rf'\1theme: "{new_theme}"', content, flags=re.MULTILINE)
|
|
with open(CONFIG_FILE, 'w', encoding='utf-8') as f:
|
|
f.write(content)
|
|
self.reload_content()
|
|
except Exception as e:
|
|
print(f"Erreur thème: {e}")
|
|
|
|
def on_key_press(self, widget, event):
|
|
if event.keyval == Gdk.KEY_Escape:
|
|
self.destroy()
|
|
return True
|
|
return False
|
|
|
|
def on_decide_policy(self, webview, decision, decision_type):
|
|
if decision_type == WebKit2.PolicyDecisionType.NAVIGATION_ACTION:
|
|
uri = decision.get_navigation_action().get_request().get_uri()
|
|
|
|
if uri == 'app://close':
|
|
self.destroy()
|
|
elif uri == 'app://toggle-theme':
|
|
self.toggle_theme()
|
|
elif uri == 'app://settings':
|
|
subprocess.Popen(['xdg-open', CONFIG_FILE])
|
|
elif uri and uri.startswith('app://run/'):
|
|
subprocess.Popen(uri.replace('app://run/', '').split())
|
|
elif uri and (uri.startswith('smb://') or uri.startswith('nfs://')):
|
|
# SMB/NFS: monter avec gio et ouvrir dans Nautilus
|
|
self.mount_and_open(uri)
|
|
elif uri and not uri.startswith('file://') and not uri.startswith('about:'):
|
|
try:
|
|
Gio.AppInfo.launch_default_for_uri(uri, None)
|
|
except Exception as e:
|
|
print(f"Erreur URI: {e}")
|
|
else:
|
|
return False
|
|
decision.ignore()
|
|
return True
|
|
return False
|
|
|
|
def mount_and_open(self, uri):
|
|
"""Monte un partage SMB/NFS avec gio et l'ouvre dans Nautilus."""
|
|
def open_nautilus():
|
|
subprocess.Popen(['nautilus', uri])
|
|
return False # Ne pas répéter
|
|
|
|
def mount_thread():
|
|
try:
|
|
# Monter le partage avec gio mount
|
|
subprocess.run(
|
|
['gio', 'mount', uri],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=30
|
|
)
|
|
except subprocess.TimeoutExpired:
|
|
print(f"Timeout montage: {uri}")
|
|
except Exception as e:
|
|
print(f"Erreur montage {uri}: {e}")
|
|
finally:
|
|
# Ouvrir dans Nautilus une seule fois
|
|
GLib.idle_add(open_nautilus)
|
|
|
|
# Lancer le montage en arrière-plan pour ne pas bloquer l'UI
|
|
threading.Thread(target=mount_thread, daemon=True).start()
|
|
|
|
|
|
def is_running():
|
|
if os.path.exists(PID_FILE):
|
|
try:
|
|
with open(PID_FILE, 'r') as f:
|
|
pid = int(f.read().strip())
|
|
os.kill(pid, 0)
|
|
return pid
|
|
except (OSError, ValueError):
|
|
pass
|
|
return None
|
|
|
|
|
|
def main():
|
|
if '--toggle' in sys.argv and is_running():
|
|
import signal
|
|
os.kill(is_running(), signal.SIGTERM)
|
|
if os.path.exists(PID_FILE):
|
|
os.remove(PID_FILE)
|
|
sys.exit(0)
|
|
|
|
if is_running():
|
|
print(f"Dashboard déjà en cours")
|
|
sys.exit(0)
|
|
|
|
os.environ['WEBKIT_DISABLE_COMPOSITING_MODE'] = '1'
|
|
|
|
with open(PID_FILE, 'w') as f:
|
|
f.write(str(os.getpid()))
|
|
|
|
import atexit
|
|
atexit.register(lambda: os.path.exists(PID_FILE) and os.remove(PID_FILE))
|
|
|
|
win = DashboardLauncherWindow()
|
|
win.show_all()
|
|
Gtk.main()
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|