Was macht nsgo?
Eine Go-UI-Engine: App-Logik, der reaktive Graph, Stile, Layout und Text laufen in Go; jede Plattform zeichnet mit ihren eigenen nativen Views. Apps werden in Go (oder TSX, das zu goja kompiliert wird) geschrieben. Design, Namen und Verhalten sind von NSH (Rust) portiert.
Eiserne Regel: Lies die API, rate sie niemals
NSGo ist eine Vorabversion, hat keine stabile API und fast keine öffentliche Dokumentation im Web, daher sind erinnerte Bezeichner fast immer falsch. Go kann bei einem falschen Feldnamen nicht kompilieren. Bevor Sie eine App schreiben, holen Sie die Wahrheit aus dem Modul selbst:
go doc github.com/oneo-me/nsgo/core # oder: go doc -all ./core
go doc github.com/oneo-me/nsgo/base
In einem Checkout des Repos sind ARCHITECTURE.md, docs/architecture/*.md (25 Fachdokumente) und examples/ maßgeblich; examples/gallery übt jedes Element, jede Komponente und jeden Dienst. Wenn Sie weder das Modul noch das Repo haben, sagen Sie das – ersetzen Sie nicht ein anderes Framework (Fyne, Gio, Wails) für die tatsächliche Anfrage des Benutzers.
Einrichtung
Erfordert Go 1.26.0+ (go.mod sagt go 1.26.0).
| Betriebssystem | Natives Ziel | Hinweise |
|---|---|---|
| macOS | ✅ | Vollständige Toolchain: Erstellt/signiert jedes Ziel mit Go + Xcode |
| Windows | ✅ | Natives + Web-Build und -Ausführung |
| Linux | ✅ | Benötigt GTK 4.14+, Pango, Cairo; WebKitGTK 6.0 und GStreamer für Web-Views/Video |
Fügen Sie das Modul und sein Tool einmal zur App hinzu – go tool nsgo führt es dann ohne Installation aus:
go get -tool github.com/oneo-me/nsgo/cmd/nsgo
NSGo veröffentlicht keine getaggten Versionen, daher löst das Modul zu einer Pseudo-Version auf (v0.0.0-2026…-46c7d9c). Einige regionale Mirror geben 404 auf ein ungetaggtes Modul zurück; wenn go get es nicht finden kann, setzen Sie den Proxy fest:
GOPROXY=https://proxy.golang.org,direct GOSUMDB=off go get -tool github.com/oneo-me/nsgo/cmd/nsgo
GOSUMDB=off wird nur benötigt, wenn sum.golang.org nicht erreichbar ist. Arbeiten Sie stattdessen von einem lokalen Checkout aus, verweisen Sie darauf und bereinigen Sie:
go mod edit -replace github.com/oneo-me/nsgo=/path/to/nsgo && go mod tidy
Ein Projekt erstellen
mkdir counter && cd counter
go mod init counter
go get -tool github.com/oneo-me/nsgo/cmd/nsgo # die Bibliothek und das Werkzeug
go tool nsgo init -name counter . # schreibt nsgo.yaml, app.go, app_test.go
go mod tidy
go tool nsgo dev . # führt es aus; baut bei jedem Speichern neu
go tool nsgo test . # headless: 2 bestanden
init benötigt einen Namen. Bloßes go tool nsgo init in einem vorhandenen Verzeichnis schlägt fehl mit "." ist kein App-Name, weil der Standardname vom Verzeichnis stammt – und in . ist das .. Übergeben Sie -name, oder übergeben Sie ein zu erstellendes Verzeichnis:
| Befehl | Effekt |
|---|---|
go tool nsgo init -name Counter . | schreibt in das aktuelle Verzeichnis, App-Name Counter |
go tool nsgo init Counter | erstellt ./Counter/ mit der App |
go tool nsgo init -template tsx Counter | erstellt ./Counter/ als TSX-App |
init gibt seinen eigenen nächsten Schritt aus (next: go tool nsgo dev Counter) – vertrauen Sie ihm mehr als Ihrem Gedächtnis.
-template ist eines von go (Standard), tsx, mixed. Der App-Name nimmt Buchstaben, Ziffern, Leerzeichen, - und _ an; die Bundle-ID wird dev.nsgo.<name>. Übergeben Sie -module, damit auch go.mod geschrieben wird.
| Vorlage | Dateien |
|---|---|
go | nsgo.yaml, app.go, app_test.go |
tsx | nsgo.yaml, main.go, App.tsx, app.test.tsx, tsconfig.json, .gitignore |
mixed | dasselbe, mit App.tsx, das eigenes Go importiert, und einer native.go |
nsgo.yaml benennt die App und ihre Ziele:
name: Hallo
id: dev.oneo.nsgo.hallo
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]
Ebenfalls gültig: build_number, appearance, theme, i18n, icon.{apple,png}, dev.icon, android.permissions, sign.{profile,keystore}, web.host. Daneben enthält resources/ ausgelieferte Dateien (geladen als asset:), assets/ enthält nur für das Projekt relevante Dateien (Symbole), locales/ enthält en.yaml, de.yaml… im ICU MessageFormat.
Die App, die Sie erhalten (-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("Gedrückt %d Mal", count.Get()) }},
base.Button{
Label: "Drück mich",
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: "Drück mich", Style: core.Style{Color: core.RGB(0xffffff)}}},
},
}}
}
func main() { nsgo.Run(Root) }
nsgo.Run nimmt entweder eine Root-View (func(cx *core.Scope) core.Node) oder eine App-Funktion (func(app *core.Scope)) entgegen, die eigene Fenster über desktop öffnet. Optionen: WindowSize(w, h), Appearance(...), HideDock(), FPS(bool), WindowMaterial(...), Fonts(paths...), Component(name, factory).
Gedankenmodell
- Signale.
core.NewSignal[T comparable](cx, v)→.Get(),.Set(v),.Update(fn).NewMemo,NewEffect. Der Graph gehört zur Engine-Goroutine und sperrt nie; andere Goroutinen betreten ihn mitScope.Post. - Elemente sind Strukturen; der Nullwert ist der Standard (wie
http.Server). Sie setzen nur, was Sie anders haben möchten. - Jedes reaktive Feld hat einen Zwilling:
Content+ContentFunc func() string,Style+StyleFunc,Class+ClassFunc.XFuncgewinnt überX; das Setzen beider führt beim Mounten zu einem Panic. Felder ohne statische Form nehmen nur die Funktion entgegen (Disabled func() bool,Selected,Expanded). - Komponenten sind jeder Typ mit
Render(cx *core.Scope) core.Node; ihre Felder sind Eigenschaften.core.Funcadaptiert eine einfache Funktion. - Regionen –
Show,Each,Dynamic,Catch– bauen nur ihren eigenen Bereich neu auf.Eachist eine generische Funktion, keine Methode. - Styling: getippte
core.Style-Werte oder Tailwind-Klassen inClass("flex-row gap-4 p-4 bg-white dark:bg-zinc-900"). Die Komponenten vonbasesind bewusst ungestylt – nur Verhalten, Zustand und Barrierefreiheit.
Bewegung
Zwei Takte. Commit = Go rendert einen Bereich neu (Signale, Show/Each/Dynamic). Frame = der Animator der Plattform bewegt AnimatedStyle-Felder und Shader-Uniforms, ohne Commit. Alle Bewegung gehört zum Frame-Takt; ein während des Renderns gelesener Wert rendert den Teilbaum neu und startet jede Animation darin neu.
| Bedarf | Verwenden Sie |
|---|---|
| Eine Zustandsänderung (Hover, Drücken, Auswahl, Größenänderung) | Style.Transition – eine Dauer, core.EaseOut und eine Properties-Bitmaske; überhaupt kein Graph und unter reduzierter Bewegung übersprungen |
| Standard-Ein-/Ausgang/Aufmerksamkeit/Schleife | core.Motion + ein core.MotionPreset (38 davon, alle respektieren reduzierte Bewegung) |
| Benutzerdefinierte Choreografie, Überlappungen, Halten, Austausche | AnimatedStyle + AnimatedNode-Graph (Timing, Spring, Interpolate, Sequence, Stagger, Loop) |
| Pixelgenau / unendliches Feld / 3D | core.Shader-Uniforms als Knoten – null Commits |
| Programmatisches Zeichnen | core.Canvas Draw (neu aufzeichnen bei Änderung) oder d.Bind (die Plattform spielt es erneut ab, keine Neuaufzeichnung) |
spec := core.FadeInUp.Spec() // oder Ihre eigene core.MotionSpec
m := core.NewMotion(cx, &spec) // nil = im Ruhezustand bauen, Play später
return core.View{Style: core.Style{Animated: m.Style()}, Children: []core.Node{card()}}
Fünf Regeln, die mehr wert sind als die API: (1) Lesen Sie niemals ein Signal oder einen animierten Wert während des Renderns – nur Closures oder AnimatedStyle; (2) stylen Sie neu anstatt neu zu bauen; (3) eine endliche Animation behält ihren letzten Wert, daher muss der Frame am Ende der lesbare sein (eine Schleife, die zu ihrem Start zurückkehrt, sieht leer aus, sobald sie sich beruhigt hat); (4) ein animiertes Element benötigt ein gemessenes Elternelement – eine vollständig absolute View misst 0×0 und schneidet alles ab; (5) AnimatedStyle-Felder sind AnimatedNodes, keine Zahlen (core.Constant(1), nicht 1), und es hat kein Width/Height/Perspective.
Lesen Sie references/animation.md, bevor Sie irgendeine Animation schreiben: Tabelle der Voreinstellungen, Easing-/Dauerwerte, die funktioniert haben, Choreografie-Rezepte, Shader-und-Canvas-Bewegung, 3D (Ray Marching), Tests und eine Symptom→Ursache-Tabelle für Bewegungsfehler.
Entwicklungs-Schleife, Build, Test
| Aufgabe | Befehl |
|---|---|
| Ausführen, bei Speichern neu bauen | go tool nsgo dev <verz> |
| Im Browser ausführen | go tool nsgo dev -target web <verz> |
| Auf Simulator/Gerät ausführen | go tool nsgo dev -target ios_simulator|android|ios <verz> |
| Jeden Commit verfolgen | ... -trace |
| Release-Pakete | go tool nsgo build <verz> (-target all, -output VERZ, -run, -device G) |
| In eine Host-App einbetten | ... build -embed → XCFramework (iOS) / AAR (Android) / nsgo.js (Web); Desktops verwenden nsgo.NewView |
| Tests | go tool nsgo test <verz> (-run REGEXP, -junit DATEI, -target macos|windows|linux|web|ios_simulator|android, -fps) |
| TSX typprüfen | go tool nsgo check <verz> |
| Nachrichtenkataloge neu generieren | go tool nsgo i18n <verz> |
Flags überschreiben die nsgo.yaml-Werte, die sie benennen. build.targets und -target akzeptieren all oder Namen wie windows_x64, macos_arm64, linux_x64, ios, web. build schreibt ein Verzeichnis pro Ziel unter -output (Standard dist), benannt <name>-<version>-<target>/. Ein Desktop-Testziel läuft nur auf diesem Betriebssystem (-target windows unter Windows, -target macos auf einem Mac); web, ios_simulator und android laufen von einem Mac.
Tests verhalten sich wie ein Benutzer. nsgotest führt sie headless ohne Fenster aus; -target führt dieselben Tests in einem echten Fenster mit nativen Eingaben und Screenshots aus (app.Screenshot("name")).
func TestMain(m *testing.M) { nsgotest.Main(m) } // gibt der Plattform den Hauptthread
func TestDrücken(t *testing.T) {
app := nsgotest.Start(t, Root)
app.Press("text:Drück mich")
app.Find("text:1 Mal gedrückt")
}
Selektoren sind die von Playwright: id:, label:, text: (ein Teilstring; text:"Speichern" oder ein bloßer String = gesamter Text), role:<rolle> <name>, view:<n>, /regexp/flags und >> zum Abstieg. Schritte: Press, PressDown/PressUp, LongPress, Key, KeyPress, Type, Focus, Hover, Scroll, ScrollTo, Drag, Touch, Back, Resize, Settle, Advance, WaitFor, WaitForGone, Exists, Query, Texts, Dump. Vollständige Liste: go doc github.com/oneo-me/nsgo/nsgotest.
TSX-Apps haben dieselbe Form: App.tsx importiert mount, signal, Text, View aus "nsgo" (Komponenten aus "nsgo/base") und endet mit mount(App); main.go bettet die Quellen ein und ruft nsgo.Run(compiler.App(src, "App.tsx")) auf. *.test.tsx-Dateien werden als Go-Tests ausgeführt. Siehe references/api.md.
Häufige Fehler
| Fehler | Realität |
|---|---|
| Feldnamen erraten | NSGo ist eine Vorabversion mit fast keiner öffentlichen Dokumentation, daher sind erinnerte Bezeichner falsch. Zuerst go doc – ein falscher Name ist ein Kompilierungsfehler |
Setzen von sowohl Content als auch ContentFunc | Panic beim Mounten. Wählen Sie eines aus |
NewSignal(cx, []string{...}) | T muss comparable sein. Verwenden Sie NewSignalFunc mit Ihrem eigenen equal |
NewSignal außerhalb eines Scopes | Signale benötigen ein cx; der Graph ist Goroutine-lokal, daher ist cx der einzige Handle. Von einer anderen Goroutine aus verwenden Sie Scope.Post |
Erwarten, dass Button.Label rendert | Label ist der barrierefreie Name (was role:/label:-Selektoren matchen). Sichtbarer Text kommt von Children |
Importieren von internal/... | Apps dürfen nur nsgo, core, base, mobile, desktop, i18n, icons, tsx, nsgotest importieren |
go build ./... unter Windows oder Linux | Schlägt fehl: internal/platform/apple/edge.go fehlt sein //go:build darwin-Tag und importiert darwin-exklusives purego/objc. Bauen Sie Ihr eigenes Paket (go build .), oder fügen Sie das Tag in einem Checkout hinzu |
JustifyContent: core.AlignCenter | Zwei Enums: AlignItems nimmt core.AlignStart/Center/End/Stretch/Baseline, JustifyContent nimmt core.JustifyStart/Center/End/SpaceBetween/.... Diese zu mischen, ist ein Kompilierungsfehler |
core.Shader{OnMouseMove: ...} | Ein Shader hat OnPress, OnLayout, Style – keinen Pointer-Move-Handler. Setzen Sie OnMouseMove func(Point) / OnHoverOut func() auf eine core.View darum herum |
MustCompileShader allein vertrauen | Kage kompiliert zur Laufzeit, daher verursacht ein schlechter Shader einen Panic beim Start, nicht beim Build. Testen Sie jede Quelle mit core.CompileShader und melden Sie seinen Fehler |
Ein Scroll, das nie scrollt und keine Leiste zeigt | Scroll hat standardmäßig FlexShrink: 1, aber ein View-Elternteil hat das nicht: Eine Spalte mit nur FlexGrow: 1 wächst auf ihren Inhalt, die Scroll-View erbt diese Höhe und nichts läuft über. Geben Sie der Spalte FlexGrow: 1, FlexShrink: 1 (React Natives flex: 1). Überprüfen Sie es: Der Rahmen des Scrolls muss kürzer als das Fenster sein |
| Nach einer permanenten Bildlaufleiste suchen | Der Indikator ist ein Overlay: Native zeichnet einen abgerundeten Daumen, der während des Scrollens einblendet, 700ms hält und dann in 300ms ausblendet; HideIndicator entfernt ihn. Im Web ist es die eigene Leiste des Browsers (overflow-y: auto, scrollbar-width: auto) |
Ein Overlay, das nur mit Left fixiert ist | Eine absolut positionierte View mit einer horizontalen Kante nimmt die verbleibende Breite ihres containing Blocks, nicht ihre Inhaltsbreite: Ein Tooltip bei Left: 34 innerhalb eines 26pt runden Ankers hat nichts mehr übrig und bricht zu einem Zeichen pro Zeile um. Geben Sie ihm eine explizite Width |
| Alpha innerhalb einer interpolierten Farbe | AnimatedStyle.Background, das von core.Interpolate mit Colors: {RGBA(0xffffff, 0.12), ...} gespeist wird, malt als nahezu festes Weiß, nicht mit 12%. Verwenden Sie undurchsichtige Farben für interpolierte Füllungen und behalten Sie Alpha für Style.Background |
MotionPreset.Name() als Bezeichnung | Gibt camelCase zurück (fadeIn, zoomIn, breathe), nicht den Namen der Go-Konstante |
| Eine Animation "springt zum Anfang zurück" | Ein Signal oder animierter Wert, der während des Renderns gelesen wird, oder NewAnimatedValue, das innerhalb des Renderings eines Dynamic erstellt wurde: Der Bereich wird neu gebaut und die Werte starten neu. Lesen Sie innerhalb … |