🐧 Client Alfred — Rapport Technique
Client WebSocket natif Linux en PyQt6 / PySide6 — Stack, architecture et packaging
1. Stack Technologique Recommandée
1.1 PyQt6 vs PySide6 Recommandé
| Critère |
PyQt6 |
PySide6 |
| Licence |
GPL v3 (commercial : commercial license) |
LGPL 2.1 (libre, commercial friendly) |
| Officialité |
Riverbank Computing (tierce) |
Qt Company (officiel) |
| Qt WebSockets |
Module séparé PyQt6.QtWebSockets |
PySide6.QtWebSockets inclus |
| Flatpak |
BaseApp disponible |
BaseApp disponible (mieux documenté) |
| Documentation |
Complexe mais bonne |
Officielle Qt, très complète |
| Écosystème |
Plus ancien, plus de code legacy |
Actif, orienté avenir |
✅ Recommandation : PySide6
Licence LGPL plus permissive, module QtWebSockets natif, base Flatpak officielle KDE mieux documentée, et c'est la direction officielle de Qt. Le code API est identique à PyQt6.
1.2 Qt WebSockets — Le module clé
Qt fournit un module QtWebSockets natif avec :
QWebSocket — Client WebSocket complet (connect, disconnect, sendTextMessage, sendBinaryMessage)
- Signaux intégrés :
connected(), disconnected(), textMessageReceived(), errorOccurred()
- Gestion automatique du protocole WebSocket (handshake, ping/pong, close frame)
- Aucune dépendance externe nécessaire — pur Qt
1.3 Packaging : Flatpak vs AppImage
| Critère |
Flatpak |
AppImage |
| Installation |
Flathub (one-click store) |
Téléchargement .AppImage + exécution |
| Distribution |
Flathub (magasins Linux) |
GitHub releases, site web |
| Sandbox |
Oui, natif (permissions) |
Non |
| Mises à jour |
Auto via le store |
Manuel ou self-update |
| Complexité |
Manifest JSON + flatpak-builder |
AppImageKit + script de build |
| Compatibilité |
Toutes les distros avec Flatpak |
Toutes les distros (binaire unique) |
✅ Recommandation : Flatpak en priorité, avec AppImage en fallback.
Flatpak offre une distribution professionnelle via Flathub, un sandboxing natif et des mises à jour automatiques. L'AppImage est un excellent fallback pour les utilisateurs sans Flatpak.
1.4 Outils de build complémentaires
PyInstaller — Pour build standalone (dev / test rapide)
fpm — Packaging RPM/DEB optionnel
flatpak-pip-generator — Génère automatiquement les deps pip pour le manifest Flatpak
2. Architecture de l'Application
2.1 Pattern architectural : MVVM / Signal-Slot Qt
Qt utilise nativement le pattern Signal-Slot (équivalent de observer/callback). Pour une architecture propre :
| Couche |
Rôle |
Composants |
| UI (View) |
Affichage, widgets, layouts |
QMainWindow, QListWidget, QTextEdit, QPushButton |
| ViewModel |
Binding UI ↔ Model |
QAbstractListModel, QStringListModel |
| Model |
Données, logique métier |
ChatHistory (JSON), SettingsManager |
| Network |
WebSocket, connexion |
QWebSocket (connecté via signaux) |
2.2 Structure du projet recommandée
alfred-client/
├── alfred_client/
│ ├── __init__.py
│ ├── main.py # Entry point (QApplication)
│ ├── main_window.py # QMainWindow principale
│ ├── chat_widget.py # Zone de chat (messages + input)
│ ├── sidebar.py # Sidebar (historique des conversations)
│ ├── connection_panel.py # Panneau connexion WebSocket
│ ├── models/
│ │ ├── __init__.py
│ │ ├── message_model.py # Modèle de message
│ │ └── chat_history.py # Gestion historique (JSON)
│ ├── network/
│ │ ├── __init__.py
│ │ └── websocket_client.py # QWebSocket wrapper
│ └── utils/
│ ├── __init__.py
│ └── settings.py # Config (QSettings)
├── resources/
│ ├── icons/
│ └── styles/
│ └── dark.qss # Thème (optionnel)
├── flatpak/
│ └── org.gdelaunay.Alfred.json # Manifest Flatpak
├── packaging/
│ ├── appimage.sh
│ └── package.sh
├── requirements.txt
├── pyproject.toml
└── README.md
2.3 UI — Composants principaux
| Composant |
Widget Qt |
Description |
| Fenêtre principale |
QMainWindow |
Layout avec splitter (sidebar + chat) |
| Historique |
QListWidget / QTreeView |
Liste des conversations avec recherche |
| Zone messages |
QListView + Delegate |
Bulles de chat (gauche/droite), timestamps |
| Input |
QTextEdit (multi-line) + QPushButton |
Zone de saisie avec bouton envoyer |
| Connexion |
QDialog / QStackedWidget |
URL WS, token auth, statut |
| Paramètres |
QSettings |
URL WS, tokens, thème, préférences |
2.4 Protocole de communication WebSocket
Format JSON proposé pour l'échange avec le serveur Alfred :
// Envoi (client → serveur)
{
"type": "message",
"content": "Bonjour Alfred",
"timestamp": "2026-06-12T17:46:00Z"
}
// Réponse (serveur → client)
{
"type": "response",
"content": "Bonjour ! Comment puis-je t'aider ?",
"timestamp": "2026-06-12T17:46:01Z"
}
// Heartbeat / statut
{
"type": "status",
"status": "online"
}
3. Exemple de Code — Socket WebSocket
#!/usr/bin/env python3
"""Alfred Client — Exemple minimal WebSocket avec PySide6"""
import sys
from PySide6.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout,
QHBoxLayout, QTextEdit, QLineEdit, QPushButton,
QListWidget, QLabel, QDialog, QFormLayout, QMessageBox
)
from PySide6.QtWebSockets import QWebSocket
from PySide6.QtCore import QUrl, Signal, QObject
from PySide6.QtGui import QFont, QIcon
# ─── WebSocket Client ───────────────────────────────
class WebSocketClient(QObject):
connected = Signal()
disconnected = Signal()
message_received = Signal(str)
error_occurred = Signal(str)
def __init__(self, parent=None):
super().__init__(parent)
self.websocket = QWebSocket()
self.websocket.connected.connect(self.on_connected)
self.websocket.disconnected.connect(self.on_disconnected)
self.websocket.textMessageReceived.connect(self.on_message)
self.websocket.errorOccurred.connect(self.on_error)
def connect_to_server(self, url: str):
self.websocket.open(QUrl(url))
def send_message(self, text: str):
self.websocket.sendTextMessage(text)
def close(self):
self.websocket.close()
# Slots
def on_connected(self):
self.connected.emit()
def on_disconnected(self):
self.disconnected.emit()
def on_message(self, message: str):
self.message_received.emit(message)
def on_error(self, error):
self.error_occurred.emit(error.string())
# ─── Fenêtre principale ─────────────────────────────
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Alfred Client")
self.resize(900, 650)
# WebSocket
self.ws_client = WebSocketClient(self)
self.ws_client.connected.connect(self.on_connected)
self.ws_client.disconnected.connect(self.on_disconnected)
self.ws_client.message_received.connect(self.on_message)
self.ws_client.error_occurred.connect(self.on_error)
self._build_ui()
def _build_ui(self):
central = QWidget()
self.setCentralWidget(central)
layout = QHBoxLayout(central)
# Sidebar — historique
self.sidebar = QListWidget()
self.sidebar.setMinimumWidth(200)
layout.addWidget(self.sidebar)
# Panneau chat
chat_panel = QWidget()
chat_layout = QVBoxLayout(chat_panel)
# Status bar
self.status_label = QLabel("Non connecté")
chat_layout.addWidget(self.status_label)
# Zone messages
self.messages = QTextEdit()
self.messages.setReadOnly(True)
self.messages.setFont(QFont('Monospace', 10))
chat_layout.addWidget(self.messages)
# Input
input_layout = QHBoxLayout()
self.input = QLineEdit()
self.input.setPlaceholderText("Message à Alfred...")
self.input.returnPressed.connect(self.send_message)
input_layout.addWidget(self.input)
self.send_btn = QPushButton("Envoyer")
self.send_btn.clicked.connect(self.send_message)
input_layout.addWidget(self.send_btn)
self.connect_btn = QPushButton("Connecter")
self.connect_btn.clicked.connect(self.show_connect_dialog)
input_layout.addWidget(self.connect_btn)
chat_layout.addLayout(input_layout)
layout.addWidget(chat_panel)
# ─── Actions ──────────────────────────────────
def send_message(self):
text = self.input.text().strip()
if not text:
return
self.ws_client.send_message(text)
self.input.clear()
def show_connect_dialog(self):
dialog = QDialog(self)
dialog.setWindowTitle("Connexion WebSocket")
form = QFormLayout(dialog)
url_input = QLineEdit("ws://localhost:8765")
form.addRow("URL WebSocket:", url_input)
btn = QPushButton("Se connecter")
btn.clicked.connect(lambda: self.connect(url_input.text()))
form.addRow(btn)
dialog.exec()
def connect(self, url: str):
self.status_label.setText("Connexion...")
self.ws_client.connect_to_server(url)
# ─── Slots UI ─────────────────────────────────
def on_connected(self):
self.status_label.setText("✅ Connecté")
self.status_label.setStyleSheet("color: green;")
def on_disconnected(self):
self.status_label.setText("❌ Déconnecté")
self.status_label.setStyleSheet("color: red;")
def on_message(self, message: str):
self.messages.append(f"Alfred: {message}")
def on_error(self, error: str):
QMessageBox.critical(self, "Erreur", error)
self.status_label.setText("❌ Erreur")
# ─── Entry point ────────────────────────────────────
if __name__ == "__main__":
app = QApplication(sys.argv)
app.setStyle("Fusion") # Look natif propre
window = MainWindow()
window.show()
sys.exit(app.exec())
4. Packaging Flatpak
4.1 Manifest (flatpak/org.gdelaunay.Alfred.json)
{
"id": "org.gdelaunay.Alfred",
"runtime": "org.kde.Platform",
"runtime-version": "6.7",
"sdk": "org.kde.Sdk",
"base": "io.qt.PySide.BaseApp",
"base-version": "6.7",
"command": "alfred-client",
"finish-args": [
"--share=ipc",
"--socket=fallback-x11",
"--socket=wayland",
"--device=dri",
"--socket=pulseaudio"
],
"modules": [
"flatpak-pip-generator --requirements requirements.txt",
{
"name": "alfred-client",
"buildsystem": "simple",
"build-commands": [
"pip3 install --no-build-isolation --prefix=/app ."
],
"sources": [
{
"type": "dir",
"path": "."
}
]
}
],
"cleanup": [
"/app/lib/python*/site-packages/*.pyc",
"/app/share/doc"
]
}
4.2 Build Flatpak
# Installer les runtimes
flatpak install flathub org.kde.Sdk/x86_64/6.7
flatpak install flathub org.kde.Platform/x86_64/6.7
flatpak install flathub io.qt.PySide.BaseApp/x86_64/6.7
# Générer les deps pip
wget https://raw.githubusercontent.com/flatpak/flatpak-builder-tools/master/pip/flatpak-pip-generator.py
python3 flatpak-pip-generator.py --requirements requirements.txt
# Builder
flatpak-builder --user --force-clean --install-deps-from=flathub build-dir org.gdelaunay.Alfred.json
# Tester
flatpak-builder --run build-dir org.gdelaunay.Alfred alfred-client
4.3 AppImage (fallback)
#!/bin/bash
# packaging/appimage.sh
cd "$(dirname "$0")/.."
# Build avec PyInstaller
pip install pyinstaller
pyinstaller --onefile --name alfred-client \
--add-data "resources:resources" \
alfred_client/main.py
# Créer l'AppImage
wget -O AppImageKit https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagex64.AppImage
chmod +x AppImageKit
./AppImageKit dist/alfred-client-x86_64.AppImage
5. Comparatif avec alfred-chat (WinUI3)
| Fonctionnalité |
alfred-chat (WinUI3) |
Alfred Client (PySide6) |
| Langage |
C# / XAML |
Python |
| Framework |
WinUI 3 (Windows uniquement) |
Qt6 (Linux/Mac/Windows) |
| WS Client |
WebSocketClient (WinRT) |
QWebSocket (natif Qt) |
| Packaging |
MSIX / MSIXpackaging |
Flatpak + AppImage |
| UI Design |
XAML (déclaratif) |
Qt Designer (.ui) + code |
| Configuration |
App.xaml / settings |
QSettings (natif Qt) |
| Complexité |
Élevée (.NET, XAML, WinRT) |
Faible (Python, Pythonic) |
💡 Avantage clé : Python + Qt6 = développement rapide, code lisible, et cross-platform natif. Le même code fonctionne sur Linux, macOS et Windows.
6. Démarché de Développement
| Étape |
Action |
Durée estimée |
| 1 |
Setup projet + structure de fichiers |
30 min |
| 2 |
Widget WebSocket client (connect/disconnect/send) |
1h |
| 3 |
UI principale (sidebar + chat area + input) |
2h |
| 4 |
Historique des conversations (JSON) |
1h |
| 5 |
Format de messages JSON + parsing |
1h |
| 6 |
Paramètres (QSettings) |
30 min |
| 7 |
Thème / style (QSS) |
1h |
| 8 |
Manifest Flatpak + build |
1h |
| 9 |
AppImage fallback |
30 min |
| 10 |
Tests + polish |
1-2h |
⏱ Total estimé : ~10-12h de développement
C'est un projet très faisable en un week-end. Le code de base tient en ~300 lignes.
7. Dépendances
# requirements.txt
PySide6>=6.6
PySide6-Addons>=6.6
PySide6-Essentials>=6.6
Pour le build Flatpak, flatpak-pip-generator générera automatiquement les bindings Qt nécessaires depuis le BaseApp.
8. Références
9. Conclusion & Recommandations
Stack finale recommandée :
PySide6 + QtWebSockets + Flatpak (+ AppImage fallback)
- ✅ PySide6 — Licence LGPL, module QtWebSockets natif, direction officielle Qt
- ✅ Flatpak — Distribution professionnelle via Flathub, sandboxing natif
- ✅ AppImage — Fallback simple pour les utilisateurs sans Flatpak
- ✅ Qt Designer — Design visuel des interfaces (fichiers .ui)
- ✅ QSettings — Persistance de config natif Qt
⚠️ Points d'attention :
- Qt WebSockets nécessite la licence Qt (LGPL pour PySide6 — OK pour usage libre)
- Flatpak nécessite les runtimes installés sur la machine cible
- La soumission sur Flathub nécessite un processus de validation (guidelines à lire)
- Penser à gérer la reconnect automatique (retry avec backoff)
🚀 Prochaine étape : Créer la structure du projet, le WebSocket client, et l'UI de base. Le code d'exemple fourni en section 3 est un point de départ fonctionnel. On peut le faire tourner en ~1h pour avoir un POC fonctionnel.