Il y a des journées de développement qui se passent exactement comme on l’espère. Celle dont je vais vous parler en fait partie. En partant de zéro — ou presque — sur une technologie que je connaissais à peine, j’ai construit en une seule session un REPL complet pour MOGWAI, tournant nativement sur Windows, Linux et macOS. Avec l’aide de mon copain Claude, bien sûr.
Le contexte : pourquoi Avalonia ?
Je développe des applications mobiles avec .NET MAUI depuis un bon moment. C’est une technologie solide, bien intégrée à l’écosystème .NET, et qui fait le travail. Mais MAUI a un angle mort de taille : Linux. Pour les applications desktop, il ne cible que Windows et macOS. Or MOGWAI tourne sur Linux — c’est même l’une de ses forces dans les contextes IoT industriels.
Avalonia m’intriguait depuis un moment. Ce framework UI .NET open source (licence MIT) cible Windows, Linux, macOS, et même iOS, Android et WebAssembly. Contrairement à MAUI, il n’est pas lié à Microsoft — ce qui, pour un vieux « devosaur » comme moi qui a vu mourir Silverlight, UWP et Xamarin, n’est pas un détail négligeable. L’écosystème Avalonia est sain : JetBrains l’utilise en interne pour Rider, la communauté est active, et le code est là sur GitHub sous licence MIT. Même si AvaloniaUI Ltd venait à disparaître demain, le projet continuerait.
Mais je n’avais jamais vraiment mis les mains dedans. Alors j’ai demandé de l’aide à mon copain Claude.
Première question : est-ce que MOGWAI est compatible avec Avalonia ?
La réponse est oui, et pour une raison très simple : le moteur MOGWAI est une bibliothèque .NET standard distribuée via NuGet, complètement découplée de toute couche UI. Il n’y a rien de spécifique à WinForms, MAUI ou autre dans le runtime. Vous instanciez MogwaiEngine, vous lui passez un IDelegate, et vous lancez RunAsync(). Peu importe que votre UI soit en WPF, MAUI, Blazor, ou Avalonia.
C’est d’ailleurs l’un des principes de conception de MOGWAI : le moteur ne sait pas dans quel contexte il tourne. C’est le IDelegate qui fait le pont entre le runtime et l’application hôte.
Mise en route : les templates Avalonia
La première étape a été d’installer les templates :
dotnet new install Avalonia.Templates
Puis de créer le projet avec le template MVVM — le pattern naturel pour Avalonia :
dotnet new avalonia.mvvm -o MogwaiRepl
cd MogwaiRepl
dotnet add package MOGWAI
À ma surprise, NuGet a téléchargé MOGWAI 8.7.0 — une version que je n’avais pas encore vue sur le repo public. Les joies du déploiement automatisé !
Pour l’IDE, j’ai utilisé VS Code avec l’extension C# Dev Kit — une première pour moi sur ce type de projet. Claude m’a guidé pas à pas : installation de l’extension, ouverture du dossier projet, configuration du debug avec F5. En quelques minutes, j’avais un environnement de développement pleinement fonctionnel, sans avoir touché à Visual Studio.
Le cœur du sujet : implémenter IDelegate
L’interface IDelegate est le contrat entre MOGWAI et l’application hôte. En version 8.7, toutes les méthodes sont obligatoires — aucune implémentation par défaut. Voici l’interface complète :
public interface IDelegate{ Task ProgramStart(MogwaiEngine engine, string code); Task ProgramEnd(MogwaiEngine engine, EvalResult result); Task<EvalResult> ConsoleClearScreen(MogwaiEngine engine); Task<EvalResult> ConsolePrintLn(MogwaiEngine engine, string message); Task<EvalResult> ConsolePrint(MogwaiEngine engine, string message); Task<EvalResult> ConsoleShow(MogwaiEngine engine); Task<EvalResult> ConsoleHide(MogwaiEngine engine); Task<EvalResult> ConsoleLocate(MogwaiEngine engine, int x, int y); Task<(EvalResult result, int x, int y)> ConsoleGetCursorPosition(MogwaiEngine engine); Task<EvalResult> ConsoleSetForegroundColor(MogwaiEngine engine, string color); Task<EvalResult> ConsoleSetBackgroundColor(MogwaiEngine engine, string color); Task<(EvalResult result, int key)> ConsoleGetInputKey(MogwaiEngine engine); Task<(EvalResult result, string? value)> Prompt(MogwaiEngine engine, string message); string[] HostFunctions(MogwaiEngine engine); Task<EvalResult> ExecuteHostFunction(MogwaiEngine engine, string word); Task<EvalResult> MessageReceivedFromRuntime(MogwaiEngine engine, string message, MOGObject parameter); Task<EvalResult> DebugMessage(MogwaiEngine engine, string message); Task<EvalResult> DebugClear(MogwaiEngine engine); Task<EvalResult> EngineDidPause(MogwaiEngine engine); Task<EvalResult> EngineDidResume(MogwaiEngine engine); Task<EvalResult> StudioDidConnect(MogwaiEngine engine); Task<EvalResult> StudioDidDisconnect(MogwaiEngine engine); Task<EvalResult> SocketServerDidStart(MogwaiEngine engine, IPAddress address, int port); Task<EvalResult> SocketServerDidStop(MogwaiEngine engine);}
La plupart de ces méthodes n’ont besoin que d’une implémentation minimale pour un REPL — on retourne EvalResult.NoError et c’est tout. Mais deux sont essentielles :
ConsolePrintLn : capturer la sortie de MOGWAI
C’est la méthode appelée chaque fois que MOGWAI exécute l’instruction ? (print). Dans un REPL graphique, il faut rediriger cette sortie vers l’UI :
public Task<EvalResult> ConsolePrintLn(MogwaiEngine engine, string message){ AddLine(message); return Task.FromResult(EvalResult.NoError);}
Thread-safety : le point critique
MOGWAI est asynchrone. Ses callbacks peuvent être appelés depuis n’importe quel thread. Dans Avalonia, comme dans tous les frameworks UI, seul le thread UI peut modifier l’interface. La solution : Dispatcher.UIThread.Post().
private void AddLine(string line) => Dispatcher.UIThread.Post(() => OutputLines.Add(line));
Cette ligne, en apparence anodine, est fondamentale. Sans elle, l’application planterait dès la première exécution d’un script asynchrone (timer, task, etc.).
L’architecture MVVM
Le pattern MVVM est le standard Avalonia, et il s’adapte parfaitement à notre cas. La MainWindowViewModel joue un double rôle : ViewModel pour le binding UI, et IDelegate pour le moteur MOGWAI.
public partial class MainWindowViewModel : ViewModelBase, IDelegate{ private MogwaiEngine _engine; [ObservableProperty] private string _inputCode = string.Empty; [ObservableProperty] private bool _isRunning; public ObservableCollection<string> OutputLines { get; } = new(); public MainWindowViewModel() { _engine = new MogwaiEngine("MogwaiRepl"); _engine.Delegate = this; } [RelayCommand] private async Task RunAsync() { IsRunning = true; var result = await _engine.RunAsync(InputCode, debugMode: false); IsRunning = false; }}
CommunityToolkit.Mvvm (inclus dans le template) génère automatiquement les propriétés et commandes bindables via les attributs [ObservableProperty] et [RelayCommand]. Aucun boilerplate à écrire.
L’interface : split éditeur / sortie
Le choix naturel pour un REPL avec un vrai éditeur de code : un layout splitté avec l’éditeur à gauche et la sortie à droite. Avalonia propose un GridSplitter qui permet de redimensionner les deux panneaux à la souris.
<Grid ColumnDefinitions="*,8,*"> <!-- Éditeur multiligne --> <Border Grid.Column="0" Background="#313244" CornerRadius="6"> <TextBox AcceptsReturn="True" AcceptsTab="True" Text="{Binding InputCode}" FontFamily="Cascadia Code,Consolas,monospace"> <TextBox.KeyBindings> <KeyBinding Gesture="Ctrl+Return" Command="{Binding RunCommand}"/> <KeyBinding Gesture="Ctrl+Up" Command="{Binding HistoryUpCommand}"/> <KeyBinding Gesture="Ctrl+Down" Command="{Binding HistoryDownCommand}"/> </TextBox.KeyBindings> </TextBox> </Border> <GridSplitter Grid.Column="1" ResizeDirection="Columns"/> <!-- Sortie --> <Border Grid.Column="2" Background="#313244" CornerRadius="6"> <ScrollViewer> <ItemsControl ItemsSource="{Binding OutputLines}"> <ItemsControl.ItemTemplate> <DataTemplate> <TextBlock Text="{Binding}" FontFamily="Cascadia Code,Consolas,monospace"/> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl> </ScrollViewer> </Border></Grid>
Le TextBox en mode multiligne accepte Ctrl+Enter pour exécuter (plutôt qu’Enter seul qui insère une nouvelle ligne). Les flèches Ctrl+↑ et Ctrl+↓ naviguent dans l’historique des commandes.

Les fonctionnalités construites en une session
Voici ce qu’on a construit, fonctionnalité par fonctionnalité :
Run / Stop / Clear
Le bouton Stop appelle _engine.Halt() — la méthode native de MOGWAI pour interrompre proprement l’exécution en cours. Le bouton est désactivé (IsEnabled="{Binding IsRunning}") quand aucun script ne tourne.
Gestion de fichiers .mog
Avalonia propose une API StorageProvider unifiée pour les dialogs de fichiers, qui fonctionne identiquement sur toutes les plateformes :
var files = await window.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions{ FileTypeFilter = new[] { new FilePickerFileType("MOGWAI Script") { Patterns = new[] { "*.mog" } } }});
Pas de code spécifique Windows, pas d’adaptation macOS/Linux — ça marche partout.
Historique des commandes
Un List<string> et deux commandes HistoryUp/HistoryDown suffisent. La navigation se fait avec Ctrl+↑ et Ctrl+↓ pour ne pas interférer avec les flèches normales dans l’éditeur multiligne.
Dialog Prompt
L’instruction prompt de MOGWAI attend une saisie utilisateur. Dans une application graphique, il faut une vraie fenêtre de dialogue. On a créé une PromptWindow Avalonia — une fenêtre modale simple avec un label, un TextBox et un bouton OK :
public Task<(EvalResult result, string? value)> Prompt(MogwaiEngine engine, string message){ var tcs = new TaskCompletionSource<(EvalResult, string?)>(); Dispatcher.UIThread.Post(async () => { var dialog = new PromptWindow(message); var result = await dialog.ShowDialog<string?>(window); tcs.SetResult((EvalResult.NoError, result)); }); return tcs.Task;}
Le TaskCompletionSource est le pattern classique pour bridger un callback UI asynchrone avec une Task attendable depuis le moteur MOGWAI.
Mode Studio
C’est peut-être la fonctionnalité la plus intéressante. MOGWAI intègre un serveur socket qui permet à l’extension VS Code de s’y connecter pour du debug live : inspection de la pile, des variables, exécution pas-à-pas. Une seule ligne suffit pour démarrer ce serveur :
await _engine.StartNetworkCommunication();
L’interface reflète l’état de la connexion via les callbacks IDelegate :
SocketServerDidStart→ le serveur écoute sur le port 1968StudioDidConnect→ l’extension VS Code est connectéeStudioDidDisconnect→ déconnexion
Un bouton 📡 Connect to Studio / ⏏ Disconnect from Studio permet de basculer entre les deux états. Le résultat : on peut déboguer en temps réel un script MOGWAI depuis VS Code, avec toute la puissance de l’extension, dans une application Avalonia.
Le polissage de l’UI
Un détail important avec Avalonia : le thème par défaut applique ses propres couleurs sur les états :hover, :focus, :pointerover des contrôles. Sur un design sombre custom, le résultat est souvent illisible. La solution : surcharger ces états via les styles Avalonia.
Pour les boutons :
<Style Selector="Button:pointerover /template/ ContentPresenter"> <Setter Property="Background" Value="{Binding $parent[Button].Background}"/> <Setter Property="Opacity" Value="0.8"/></Style><Style Selector="Button:disabled /template/ ContentPresenter"> <Setter Property="Background" Value="{Binding $parent[Button].Background}"/> <Setter Property="Opacity" Value="0.4"/></Style>
Pour le TextBox (qui changeait de couleur à chaque état) :
<Style Selector="TextBox /template/ Border#PART_BorderElement"> <Setter Property="Background" Value="Transparent"/> <Setter Property="BorderThickness" Value="0"/></Style>
Ces quelques lignes de style suffisent à obtenir un comportement cohérent sur tous les états interactifs.
Déploiement multiplateforme
C’est là qu’Avalonia montre vraiment sa valeur. Une seule commande dotnet publish avec le bon RuntimeIdentifier produit un exécutable autonome qui embarque le runtime .NET — aucune installation requise sur la machine cible :
# Windows
dotnet publish -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true
# Linux
dotnet publish -c Release -r linux-x64 --self-contained true -p:PublishSingleFile=true
# macOS Apple Silicon
dotnet publish -c Release -r osx-arm64 --self-contained true -p:PublishSingleFile=true
J’ai pu envoyer les binaires à mes fils pour tests — l’un sur Linux, l’autre sur Mac Apple Silicon — sans qu’ils aient besoin d’installer quoi que ce soit. Sur macOS, une simple commande xattr -cr MogwaiRepl contourne Gatekeeper pour les binaires non signés.
Ce qu’on a appris sur Avalonia
Quelques observations après cette première vraie session avec Avalonia :
Le bon : le modèle XAML + MVVM est très proche de WPF. Si vous venez de WPF ou de MAUI, vous êtes à la maison en moins d’une heure. Les bindings, les commandes, les ObservableCollection — tout fonctionne exactement comme attendu.
Le moins bon : le thème par défaut peut être surprenant sur un design custom. Les états :hover, :focus et :disabled nécessitent une attention particulière si on sort du thème standard. Rien d’insurmontable, mais à anticiper.
La bonne surprise : la gestion cross-plateforme est vraiment transparente. Le StorageProvider pour les dialogs de fichiers, le Dispatcher.UIThread pour les mises à jour UI — tout est unifié. Pas de code conditionnel selon la plateforme.
L’absence notable : pas de coloration syntaxique native dans le TextBox. Pour aller plus loin, il faudra intégrer AvalonEdit — c’est d’ailleurs la prochaine étape prévue pour ce projet.
Bilan : une session, un outil complet
En une journée de travail — en partant de zéro sur Avalonia — voici ce qu’on a construit :
- Éditeur de code multiligne avec raccourcis clavier
- Exécution / interruption des scripts
- Chargement et sauvegarde de fichiers
.mog - Historique de commandes
- Dialog
promptnatif - Mode Studio avec connexion VS Code
- Déploiement autonome Windows / Linux / macOS
Le tout en environ 300 lignes de C# et 100 lignes de XAML.
MOGWAI s’intègre dans Avalonia exactement comme dans n’importe quelle autre application .NET. Le IDelegate fait son travail, le Dispatcher.UIThread gère la thread-safety, et le mode Studio fonctionne sans modification côté moteur. C’est la force d’une architecture bien découplée.
Le projet est disponible comme exemple dans le repo MOGWAI.
Pour en savoir plus sur MOGWAI : mogwai.eu.com
Pour tester en ligne : Blazor REPL