🐧 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 :

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

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

Ressource Lien Utilité
PySide6 QtWebSockets — API officielle doc.qt.io/qtforpython-6/PySide6/QtWebSockets/ Documentation complète de l'API WebSocket
Exemple de chat WebSocket Qt doc.qt.io/qt-6/qtwebsockets-simplechat-example.html Exemple officiel minimal fonctionnel
Guide Flatpak PyQt6 — KDE Developer develop.kde.org/docs/getting-started/python/python-flatpak/ Guide officiel de packaging Flatpak pour PyQt/PySide
PySide6 — Exemples officiels github.com/PySide/examples Exemples officiels Qt for Python
LLM Chat Client — PyQt6 (GitHub) github.com/adomdre/llm-chat-client-PyQt6 Implémentation complète de chat LLM en PyQt6
Flatpak Pip Generator github.com/flatpak/flatpak-builder-tools Générer automatiquement les deps pip pour Flatpak
Qt Designer — Design visuel doc.qt.io/qt-designer-manual.html Guide officiel Qt Designer
AppImage — Guide de packaging docs.appimage.org/packaging-guide/ Guide officiel AppImage

9. Conclusion & Recommandations

Stack finale recommandée :
PySide6 + QtWebSockets + Flatpak (+ AppImage fallback)
⚠️ Points d'attention :
🚀 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.