Que fait nsgo ?
Un moteur d'interface utilisateur Go : la logique applicative, le graphe réactif, les styles, la mise en page et le texte s'exécutent en Go ; chaque plateforme dessine avec ses propres vues natives. Les applications sont écrites en Go (ou en TSX compilé vers goja). La conception, les noms et le comportement sont portés de NSH (Rust).
Règle d'or : lisez l'API, ne devinez jamais
NSGo est en pré-version, n'a pas d'API stable et presque aucune documentation publique sur le web, donc les identifiants dont vous vous souvenez sont presque toujours faux. Go ne compile pas si un nom de champ est erroné. Avant d'écrire une application, obtenez la vérité à partir du module lui-même :
go doc github.com/oneo-me/nsgo/core # ou : go doc -all ./core
go doc github.com/oneo-me/nsgo/base
Dans un répertoire extrait du dépôt, ARCHITECTURE.md, docs/architecture/*.md (25 documents thématiques) et examples/ font autorité ; examples/gallery utilise chaque élément, composant et service. Si vous n'avez ni le module ni le dépôt, dites-le — ne substituez pas un autre framework (Fyne, Gio, Wails) à la demande réelle de l'utilisateur.
Configuration
Nécessite Go 1.26.0+ (go.mod indique go 1.26.0).
| OS | Cible native | Remarques |
|---|---|---|
| macOS | ✅ | Chaîne d'outils complète : construit/signe toutes les cibles avec Go + Xcode |
| Windows | ✅ | Construction et exécution natives + web |
| Linux | ✅ | Nécessite GTK 4.14+, Pango, Cairo ; WebKitGTK 6.0 et GStreamer pour les vues web/vidéo |
Ajoutez le module et son outil à l'application une fois — go tool nsgo l'exécute alors sans installation :
go get -tool github.com/oneo-me/nsgo/cmd/nsgo
NSGo ne publie aucune version taguée, donc le module se résout en une pseudo-version (v0.0.0-2026…-46c7d9c). Certains miroirs régionaux renvoient une erreur 404 sur un module non tagué ; si go get ne le trouve pas, épinglez le proxy :
GOPROXY=https://proxy.golang.org,direct GOSUMDB=off go get -tool github.com/oneo-me/nsgo/cmd/nsgo
GOSUMDB=off n'est nécessaire que si sum.golang.org est inaccessible. Pour travailler à partir d'une copie locale du dépôt, pointez vers elle et nettoyez :
go mod edit -replace github.com/oneo-me/nsgo=/chemin/vers/nsgo && go mod tidy
Créer un projet
mkdir counter && cd counter
go mod init counter
go get -tool github.com/oneo-me/nsgo/cmd/nsgo # la bibliothèque et l'outil
go tool nsgo init -name counter . # écrit nsgo.yaml, app.go, app_test.go
go mod tidy
go tool nsgo dev . # l'exécute ; reconstruit à chaque sauvegarde
go tool nsgo test . # sans tête : 2 réussis
init a besoin d'un nom. go tool nsgo init nu dans un répertoire existant échoue avec "." n'est pas un nom d'application, car le nom par défaut provient du répertoire — et dans . c'est .. Passez -name, ou passez un répertoire à créer :
| Commande | Effet |
|---|---|
go tool nsgo init -name Counter . | écrit dans le répertoire courant, application nommée Counter |
go tool nsgo init Counter | crée ./Counter/ contenant l'application |
go tool nsgo init -template tsx Counter | crée ./Counter/ comme une application TSX |
init affiche sa propre prochaine étape (next: go tool nsgo dev Counter) — faites-lui confiance plutôt qu'à votre mémoire.
-template est l'un de go (par défaut), tsx, mixed. Le nom de l'application accepte les lettres, les chiffres, les espaces, - et _ ; l'identifiant du bundle devient dev.nsgo.<nom>. Passez -module pour que go.mod soit également écrit.
| Modèle | Fichiers |
|---|---|
go | nsgo.yaml, app.go, app_test.go |
tsx | nsgo.yaml, main.go, App.tsx, app.test.tsx, tsconfig.json, .gitignore |
mixed | les mêmes, avec App.tsx important son propre Go et un native.go |
nsgo.yaml nomme l'application et ses cibles :
name: Hello
id: dev.oneo.nsgo.hello
version: 0.1.0
dev:
target: native # native | web | ios | ios_simulator | android
hot_reload: true
fps: false
build:
output: dist
targets: [macos_arm64, windows_x64, ios_simulator, android_arm64, web]
Sont également valides : build_number, appearance, theme, i18n, icon.{apple,png}, dev.icon, android.permissions, sign.{profile,keystore}, web.host. À côté, resources/ contient les fichiers livrés (chargés comme asset:), assets/ contient les fichiers propres au projet (icônes), locales/ contient en.yaml, de.yaml… au format ICU MessageFormat.
L'application que vous obtenez (-template go)
import (
"fmt"
"github.com/oneo-me/nsgo"
"github.com/oneo-me/nsgo/base"
"github.com/oneo-me/nsgo/core"
)
func Root(cx *core.Scope) core.Node {
count := core.NewSignal(cx, 0)
return core.View{Style: core.Style{FlexGrow: 1, Padding: core.All(24), Gap: 16}, Children: []core.Node{
core.Text{ContentFunc: func() string { return fmt.Sprintf("Pressed %d times", count.Get()) }},
base.Button{
Label: "Press me",
OnPress: func(core.PressEvent) { count.Update(func(n int) int { return n + 1 }) },
Style: core.Style{Background: core.RGB(0x0969da), CornerRadius: 8, Padding: core.Symmetric(10, 16)},
Children: []core.Node{core.Text{Content: "Press me", Style: core.Style{Color: core.RGB(0xffffff)}}},
},
}}
}
func main() { nsgo.Run(Root) }
nsgo.Run prend soit une vue racine (func(cx *core.Scope) core.Node) soit une fonction d'application (func(app *core.Scope)) qui ouvre ses propres fenêtres via desktop. Options : WindowSize(w, h), Appearance(...), HideDock(), FPS(bool), WindowMaterial(...), Fonts(chemins...), Component(nom, fabrique).
Modèle mental
- Signaux.
core.NewSignal[T comparable](cx, v)→.Get(),.Set(v),.Update(fn).NewMemo,NewEffect. Le graphe appartient à la goroutine du moteur et ne se verrouille jamais ; les autres goroutines y accèdent avecScope.Post. - Les éléments sont des structs ; la valeur zéro est la valeur par défaut (comme
http.Server). Vous ne définissez que ce que vous voulez différencier. - Chaque champ réactif a un jumeau :
Content+ContentFunc func() string,Style+StyleFunc,Class+ClassFunc.XFuncl'emporte surX; définir les deux panique au montage. Les champs sans forme statique ne prennent que la fonction (Disabled func() bool,Selected,Expanded). - Les composants sont tout type avec
Render(cx *core.Scope) core.Node; leurs champs sont des propriétés.core.Funcadapte une fonction simple. - Régions —
Show,Each,Dynamic,Catch— ne reconstruisent que leur propre étendue.Eachest une fonction générique, pas une méthode. - Stylisation : valeurs typées
core.Style, ou classes Tailwind dansClass("flex-row gap-4 p-4 bg-white dark:bg-zinc-900"). Les composants debasesont délibérément non stylés — comportement, état et accessibilité uniquement.
Animation
Deux horloges. Commit = Go re-rend une étendue (signaux, Show/Each/Dynamic). Frame = l'animateur de la plateforme fait progresser les champs AnimatedStyle et les uniforms du shader, sans commit. Tout mouvement appartient à l'horloge de frame ; une valeur lue pendant le rendu re-rend la sous-arborescence et redémarre chaque animation qu'elle contient.
| Besoin | Utilisez |
|---|---|
| Un changement d'état (survol, pression, sélection, redimensionnement) | Style.Transition — une durée, core.EaseOut, et un masque de bits Properties ; pas de graphe du tout, et ignoré en cas de mouvement réduit |
| Entrée/sortie/attention/boucle standard | core.Motion + un core.MotionPreset (38, tous respectent le mouvement réduit) |
| Chorégraphie personnalisée, chevauchement, maintien, échanges | AnimatedStyle + graphe AnimatedNode (Timing, Spring, Interpolate, Sequence, Stagger, Loop) |
| Par pixel / champ infini / 3D | Uniforms core.Shader en tant que nœuds — zéro commits |
| Dessin procédural | core.Canvas Draw (ré-enregistre par changement) ou d.Bind (la plateforme le rejoue, pas de ré-enregistrement) |
spec := core.FadeInUp.Spec() // ou votre propre core.MotionSpec
m := core.NewMotion(cx, &spec) // nil = construit au repos, Play plus tard
return core.View{Style: core.Style{Animated: m.Style()}, Children: []core.Node{card()}}
Cinq règles qui valent plus que l'API : (1) ne lisez jamais un signal ou une valeur animée dans le rendu — uniquement des closures ou AnimatedStyle ; (2) restylez au lieu de reconstruire ; (3) une animation finie conserve sa dernière valeur, donc la frame à la fin doit être celle qui est lisible (une boucle qui revient à son début semble vide une fois stabilisée) ; (4) un élément animé a besoin d'un parent mesuré — une View entièrement en absolute mesure 0×0 et coupe tout ; (5) les champs AnimatedStyle sont des AnimatedNodes, pas des nombres (core.Constant(1), pas 1), et il n'a pas de Width/Height/Perspective.
Lisez references/animation.md avant d'écrire une animation : tableau des préréglages, valeurs d'assouplissement/durée qui ont fonctionné, recettes de chorégraphie, mouvement par shader et canvas, 3D (ray marching), tests, et une table symptôme→cause pour les bugs d'animation.
Boucle de développement, construction, test
| Tâche | Commande |
|---|---|
| Exécuter, reconstruire à la sauvegarde | go tool nsgo dev <rép> |
| Exécuter dans un navigateur | go tool nsgo dev -target web <rép> |
| Exécuter sur simulateur/appareil | go tool nsgo dev -target ios_simulator|android|ios <rép> |
| Tracer chaque commit | ... -trace |
| Paquets de version | go tool nsgo build <rép> (-target all, -output DIR, -run, -device D) |
| Intégrer dans une application hôte | ... build -embed → XCFramework (iOS) / AAR (Android) / nsgo.js (web) ; les bureaux utilisent nsgo.NewView |
| Tests | go tool nsgo test <rép> (-run REGEXP, -junit FILE, -target macos|windows|linux|web|ios_simulator|android, -fps) |
| Vérifier le typage TSX | go tool nsgo check <rép> |
| Régénérer les catalogues de messages | go tool nsgo i18n <rép> |
Les drapeaux remplacent les valeurs de nsgo.yaml qu'ils nomment. build.targets et -target acceptent all, ou des noms comme windows_x64, macos_arm64, linux_x64, ios, web. build écrit un répertoire par cible sous -output (par défaut dist), nommé <nom>-<version>-<cible>/. Une cible de test de bureau ne s'exécute que sur ce système d'exploitation (-target windows sur Windows, -target macos sur un Mac) ; web, ios_simulator et android s'exécutent depuis un Mac.
Les tests agissent comme un utilisateur. nsgotest les exécute sans tête (sans fenêtre) ; -target exécute les mêmes tests dans une vraie fenêtre avec des entrées natives et des captures d'écran (app.Screenshot("nom")).
func TestMain(m *testing.M) { nsgotest.Main(m) } // donne à la plateforme le thread principal
func TestPress(t *testing.T) {
app := nsgotest.Start(t, Root)
app.Press("text:Press me")
app.Find("text:Pressed 1 times")
}
Les sélecteurs sont ceux de Playwright : id:, label:, text: (une sous-chaîne ; text:"Save" ou une chaîne nue = texte entier), role:<rôle> <nom>, view:<n>, /regexp/drapeaux, et >> pour descendre. Étapes : Press, PressDown/PressUp, LongPress, Key, KeyPress, Type, Focus, Hover, Scroll, ScrollTo, Drag, Touch, Back, Resize, Settle, Advance, WaitFor, WaitForGone, Exists, Query, Texts, Dump. Liste complète : go doc github.com/oneo-me/nsgo/nsgotest.
Les applications TSX ont la même forme : App.tsx importe mount, signal, Text, View depuis "nsgo" (les composants depuis "nsgo/base") et se termine par mount(App) ; main.go intègre les sources et appelle nsgo.Run(compiler.App(src, "App.tsx")). Les fichiers *.test.tsx s'exécutent comme des tests Go. Voir references/api.md.
Erreurs fréquentes
| Erreur | Réalité |
|---|---|
| Deviner les noms de champs | NSGo est en pré-version avec presque aucune documentation publique, donc les identifiants dont vous vous souvenez sont faux. go doc d'abord — un seul nom erroné est une erreur de compilation |
Définir à la fois Content et ContentFunc | Panique au montage. Choisissez-en un |
NewSignal(cx, []string{...}) | T doit être comparable. Utilisez NewSignalFunc avec votre propre equal |
NewSignal en dehors d'une portée | Les signaux ont besoin d'un cx ; le graphe est local à la goroutine, donc cx est la seule référence. Depuis une autre goroutine, utilisez Scope.Post |
S'attendre à ce que Button.Label s'affiche | Label est le nom accessible (ce que les sélecteurs role:/label: correspondent). Le texte visible provient de Children |
Importer internal/... | Les applications ne peuvent importer que nsgo, core, base, mobile, desktop, i18n, icons, tsx, nsgotest |
go build ./... sur Windows ou Linux | Échoue : internal/platform/apple/edge.go n'a pas sa balise //go:build darwin et importe purego/objc uniquement darwin. Construisez votre propre paquet (go build .), ou ajoutez la balise dans un répertoire extrait |
JustifyContent: core.AlignCenter | Deux énumérations : AlignItems prend core.AlignStart/Center/End/Stretch/Baseline, JustifyContent prend core.JustifyStart/Center/End/SpaceBetween/.... Les mélanger est une erreur de compilation |
core.Shader{OnMouseMove: ...} | Un shader a OnPress, OnLayout, Style — pas de gestionnaire de mouvement de pointeur. Mettez OnMouseMove func(Point) / OnHoverOut func() sur un core.View autour de lui |
Faire confiance à MustCompileShader seul | Kage compile à l'exécution, donc un mauvais shader panique au démarrage, pas à la construction. Testez chaque source avec core.CompileShader et signalez son erreur |
Un Scroll qui ne défile jamais et n'affiche pas de barre | Scroll par défaut à FlexShrink: 1, mais un parent View ne le fait pas : une colonne avec seulement FlexGrow: 1 grandit jusqu'à son contenu, la vue de défilement hérite de cette hauteur et rien ne déborde. Donnez à la colonne FlexGrow: 1, FlexShrink: 1 (le flex: 1 de React Native). Vérifiez-le : le cadre du défilement doit être plus court que la fenêtre |
| Chercher une barre de défilement permanente | L'indicateur est une superposition : le natif peint un pouce arrondi qui s'estompe lors du défilement, se maintient 700 ms, puis s'estompe en 300 ms ; HideIndicator le supprime. Sur le web, c'est la barre du navigateur lui-même (overflow-y: auto, scrollbar-width: auto) |
Une superposition ancrée avec Left uniquement | Une vue positionnée en absolute avec un bord horizontal prend la largeur restante de son bloc conteneur, pas sa largeur de contenu : une infobulle à Left: 34 à l'intérieur d'un ancrage rond de 26pt n'a plus rien et s'enroule à un caractère par ligne. Donnez-lui une Width explicite |
| Alpha à l'intérieur d'une couleur interpolée | AnimatedStyle.Background alimenté par core.Interpolate avec Colors: {RGBA(0xffffff, 0.12), ...} peint comme un blanc quasi solide, pas 12 %. Utilisez des couleurs opaques pour les remplissages interpolés et conservez l'alpha pour Style.Background |
MotionPreset.Name() comme étiquette | Renvoie camelCase (fadeIn, zoomIn, breathe), pas le nom de la constante Go |
| Une animation "saute au début" | Un signal ou une valeur animée lu pendant le rendu, ou NewAnimatedValue construit à l'intérieur du rendu d'un Dynamic : l'étendue se reconstruit et les valeurs redémarrent. Lisez à l'intérieur |