Communitygithub.com

HycJack/nsgo

Utilisez-le pour écrire, exécuter, tester, empaqueter ou déboguer une application NSGo. NSGo (github.com/oneo-me/nsgo) est un moteur d'interface native multiplateforme avec un backend Go — le port Go de Native Shell (NSH) — ciblant macOS, iOS, Windows, Linux, Android et le web.

Qu'est-ce que nsgo ?

NSGo est un moteur d'interface utilisateur natif écrit en Go qui permet de créer des applications multiplateformes (macOS, iOS, Windows, Linux, Android et Web) en utilisant une logique Go et des vues natives pour chaque plateforme. Il propose une architecture réactive basée sur des signaux, des composants, des animations et un style typé. L'outillage en ligne de commande `go tool nsgo` facilite l'initialisation de projets, le développement avec rechargement à chaud, les tests, la construction et l'empaquetage. Il prend en charge les applications en Go pur, ainsi que les applications TSX compilées via goja. La bibliothèque est en pré-version, sans API stable ni documentation en ligne exhaustive, il est donc impératif de consulter la documentation du module (`go doc`) pour obtenir les identifiants corrects des types et des champs, sous peine d'erreurs de compilation. L'agent doit impérativement lire la documentation avant d'écrire du code.

Compatible avec~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/HycJack/nsgo-motion-lab/tree/HEAD

Demander à votre IA préférée

Ouvre une nouvelle conversation avec cette compétence d'agent déjà préchargée.

Documentation

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).

OSCible nativeRemarques
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 :

CommandeEffet
go tool nsgo init -name Counter .écrit dans le répertoire courant, application nommée Counter
go tool nsgo init Countercrée ./Counter/ contenant l'application
go tool nsgo init -template tsx Countercré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èleFichiers
gonsgo.yaml, app.go, app_test.go
tsxnsgo.yaml, main.go, App.tsx, app.test.tsx, tsconfig.json, .gitignore
mixedles 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 avec Scope.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. XFunc l'emporte sur X ; 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.Func adapte une fonction simple.
  • Régions — Show, Each, Dynamic, Catch — ne reconstruisent que leur propre étendue. Each est une fonction générique, pas une méthode.
  • Stylisation : valeurs typées core.Style, ou classes Tailwind dans Class ("flex-row gap-4 p-4 bg-white dark:bg-zinc-900"). Les composants de base sont 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.

BesoinUtilisez
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 standardcore.Motion + un core.MotionPreset (38, tous respectent le mouvement réduit)
Chorégraphie personnalisée, chevauchement, maintien, échangesAnimatedStyle + graphe AnimatedNode (Timing, Spring, Interpolate, Sequence, Stagger, Loop)
Par pixel / champ infini / 3DUniforms core.Shader en tant que nœuds — zéro commits
Dessin procéduralcore.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âcheCommande
Exécuter, reconstruire à la sauvegardego tool nsgo dev <rép>
Exécuter dans un navigateurgo tool nsgo dev -target web <rép>
Exécuter sur simulateur/appareilgo tool nsgo dev -target ios_simulator|android|ios <rép>
Tracer chaque commit... -trace
Paquets de versiongo 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
Testsgo tool nsgo test <rép> (-run REGEXP, -junit FILE, -target macos|windows|linux|web|ios_simulator|android, -fps)
Vérifier le typage TSXgo tool nsgo check <rép>
Régénérer les catalogues de messagesgo 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

ErreurRéalité
Deviner les noms de champsNSGo 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 ContentFuncPanique au montage. Choisissez-en un
NewSignal(cx, []string{...})T doit être comparable. Utilisez NewSignalFunc avec votre propre equal
NewSignal en dehors d'une portéeLes 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'afficheLabel 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.AlignCenterDeux é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 seulKage 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 barreScroll 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 permanenteL'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 uniquementUne 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éeAnimatedStyle.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 étiquetteRenvoie 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

Skills associés