¿Qué hace nsgo?
Un motor de UI en Go: la lógica de la aplicación, el grafo reactivo, los estilos, el diseño y el texto se ejecutan en Go; cada plataforma dibuja con sus propias vistas nativas. Las aplicaciones se escriben en Go (o TSX compilado a goja). El diseño, los nombres y el comportamiento son un puerto de NSH (Rust).
Regla de hierro: lee la API, nunca la adivines
NSGo es prelanzamiento, no tiene API estable y casi no tiene documentación pública en la web, por lo que los identificadores recordados casi siempre son incorrectos. Go falla al compilar con un solo nombre de campo incorrecto. Antes de escribir una aplicación, obtén la verdad del propio módulo:
go doc github.com/oneo-me/nsgo/core # o: go doc -all ./core
go doc github.com/oneo-me/nsgo/base
En un checkout del repositorio, ARCHITECTURE.md, docs/architecture/*.md (25 documentos temáticos) y examples/ son autoritativos; examples/gallery ejercita todos los elementos, componentes y servicios. Si no tienes ni el módulo ni el repositorio, dilo — no sustituyas otro framework (Fyne, Gio, Wails) por la solicitud real del usuario.
Configuración
Requiere Go 1.26.0+ (go.mod dice go 1.26.0).
| SO | Destino nativo | Notas |
|---|---|---|
| macOS | ✅ | Cadena de herramientas completa: construye/firma cada destino con Go + Xcode |
| Windows | ✅ | Construcción y ejecución nativa y web |
| Linux | ✅ | Necesita GTK 4.14+, Pango, Cairo; WebKitGTK 6.0 y GStreamer para vistas web/vídeo |
Añade el módulo y su herramienta a la aplicación una vez — go tool nsgo luego la ejecuta sin instalar:
go get -tool github.com/oneo-me/nsgo/cmd/nsgo
NSGo no publica versiones etiquetadas, por lo que el módulo se resuelve a una pseudoversión (v0.0.0-2026…-46c7d9c). Algunos mirrors regionales devuelven 404 en un módulo sin etiqueta; si go get no puede encontrarlo, fija el proxy:
GOPROXY=https://proxy.golang.org,direct GOSUMDB=off go get -tool github.com/oneo-me/nsgo/cmd/nsgo
GOSUMDB=off solo es necesario donde sum.golang.org es inalcanzable. Si trabajas desde un checkout local, apunta a él y ordena:
go mod edit -replace github.com/oneo-me/nsgo=/path/to/nsgo && go mod tidy
Crear un proyecto
mkdir contador && cd contador
go mod init contador
go get -tool github.com/oneo-me/nsgo/cmd/nsgo # la librería y la herramienta
go tool nsgo init -name contador . # escribe nsgo.yaml, app.go, app_test.go
go mod tidy
go tool nsgo dev . # lo ejecuta; reconstruye en cada guardado
go tool nsgo test . # sin cabeza: 2 aprobados
init necesita un nombre. go tool nsgo init solo en un directorio existente falla con "." is not an app name, porque el nombre por defecto viene del directorio — y en . eso es .. Pasa -name, o pasa un directorio a crear:
| Comando | Efecto |
|---|---|
go tool nsgo init -name Contador . | escribe en el directorio actual, aplicación llamada Contador |
go tool nsgo init Contador | crea ./Contador/ con la aplicación |
go tool nsgo init -template tsx Contador | crea ./Contador/ como una aplicación TSX |
init imprime su propio siguiente paso (next: go tool nsgo dev Contador) — confía en él más que en la memoria.
-template es uno de go (por defecto), tsx, mixed. El nombre de la aplicación acepta letras, dígitos, espacios, - y _; el bundle id se convierte en dev.nsgo.<nombre>. Pasa -module para que se escriba también go.mod.
| Plantilla | Archivos |
|---|---|
go | nsgo.yaml, app.go, app_test.go |
tsx | nsgo.yaml, main.go, App.tsx, app.test.tsx, tsconfig.json, .gitignore |
mixed | lo mismo, con App.tsx importando Go propio y un native.go |
nsgo.yaml nombra la aplicación y sus destinos:
name: Hola
id: dev.oneo.nsgo.hola
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]
También válido: build_number, appearance, theme, i18n, icon.{apple,png}, dev.icon, android.permissions, sign.{profile,keystore}, web.host. Junto a él, resources/ contiene archivos enviados (cargados como asset:), assets/ contiene archivos solo de proyecto (iconos), locales/ contiene en.yaml, de.yaml… en ICU MessageFormat.
La aplicación que obtienes (-template go)
import (
"fmt"
"github.com/oneo-me/nsgo"
"github.com/oneo-me/nsgo/base"
"github.com/oneo-me/nsgo/core"
)
func Raiz(cx *core.Scope) core.Node {
contador := 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("Presionado %d veces", contador.Get()) }},
base.Button{
Label: "Presióname",
OnPress: func(core.PressEvent) { contador.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: "Presióname", Style: core.Style{Color: core.RGB(0xffffff)}}},
},
}}
}
func main() { nsgo.Run(Raiz) }
nsgo.Run toma una vista raíz (func(cx *core.Scope) core.Node) o una función de aplicación (func(app *core.Scope)) que abre sus propias ventanas a través de desktop. Opciones: WindowSize(w, h), Appearance(...), HideDock(), FPS(bool), WindowMaterial(...), Fonts(paths...), Component(name, factory).
Modelo mental
- Señales.
core.NewSignal[T comparable](cx, v)→.Get(),.Set(v),.Update(fn).NewMemo,NewEffect. El grafo pertenece a la gorutina del motor y nunca se bloquea; otras gorutinas entran conScope.Post. - Los elementos son structs; el valor cero es el predeterminado (como
http.Server). Solo estableces lo que quieres que sea diferente. - Cada campo reactivo tiene un gemelo:
Content+ContentFunc func() string,Style+StyleFunc,Class+ClassFunc.XFuncgana sobreX; establecer ambos causa pánico al montar. Los campos sin forma estática solo toman la función (Disabled func() bool,Selected,Expanded). - Componentes son cualquier tipo con
Render(cx *core.Scope) core.Node; sus campos son props.core.Funcadapta una función simple. - Regiones —
Show,Each,Dynamic,Catch— reconstruyen solo su propio tramo.Eaches una función genérica, no un método. - Estilizado: valores tipados
core.Style, o clases Tailwind enClass("flex-row gap-4 p-4 bg-white dark:bg-zinc-900"). Los componentes debaseestán deliberadamente sin estilo — solo comportamiento, estado y accesibilidad.
Movimiento
Dos relojes. Commit = Go vuelve a renderizar un tramo (señales, Show/Each/Dynamic). Frame = el animador de la plataforma avanza los campos AnimatedStyle y los uniforms del shader, sin commit. Todo el movimiento pertenece al reloj de frame; un valor leído durante el renderizado vuelve a renderizar el subárbol y reinicia cada animación en él.
| Necesidad | Usa |
|---|---|
| Un cambio de estado (hover, press, selección, redimensionar) | Style.Transition — una duración, core.EaseOut, y una máscara de bits Properties; sin grafo en absoluto, y se omite bajo reduce-motion |
| Entrada/salida/atención/bucle estándar | core.Motion + un core.MotionPreset (38 de ellos, todos respetan reduce-motion) |
| Coreografía personalizada, superposición, pausas, intercambios | AnimatedStyle + grafo AnimatedNode (Timing, Spring, Interpolate, Sequence, Stagger, Loop) |
| Campo por píxel / infinito / 3D | Uniforms core.Shader como nodos — cero commits |
| Dibujo procedural | core.Canvas Draw (vuelve a grabar por cambio) o d.Bind (la plataforma lo reproduce, sin regrabación) |
espec := core.FadeInUp.Spec() // o tu propio core.MotionSpec
m := core.NewMotion(cx, &spec) // nil = construir en reposo, Play más tarde
return core.View{Style: core.Style{Animated: m.Style()}, Children: []core.Node{tarjeta()}}
Cinco reglas que valen más que la API: (1) nunca leas una señal o valor animado en el renderizado — solo closures o AnimatedStyle; (2) cambia de estilo en lugar de reconstruir; (3) una animación finita mantiene su último valor, por lo que el frame al final debe ser el legible (un bucle que vuelve al inicio se ve en blanco una vez que se estabiliza); (4) un elemento animado necesita un padre medido — una View completamente absoluta mide 0×0 y recorta todo; (5) los campos AnimatedStyle son AnimatedNodes, no números (core.Constant(1), no 1), y no tiene Width/Height/Perspective.
Lee references/animation.md antes de escribir cualquier animación: tabla de presets, números de easing/duración que funcionaron, recetas de coreografía, movimiento con shader y canvas, 3D (ray marching), pruebas, y una tabla de síntoma→causa para errores de movimiento.
Bucle de desarrollo, construcción, pruebas
| Tarea | Comando |
|---|---|
| Ejecutar, reconstruir al guardar | go tool nsgo dev <dir> |
| Ejecutar en un navegador | go tool nsgo dev -target web <dir> |
| Ejecutar en simulador/dispositivo | go tool nsgo dev -target ios_simulator|android|ios <dir> |
| Rastrear cada commit | ... -trace |
| Paquetes de lanzamiento | go tool nsgo build <dir> (-target all, -output DIR, -run, -device D) |
| Incrustar en una aplicación anfitriona | ... build -embed → XCFramework (iOS) / AAR (Android) / nsgo.js (web); los escritorios usan nsgo.NewView |
| Pruebas | go tool nsgo test <dir> (-run REGEXP, -junit FILE, -target macos|windows|linux|web|ios_simulator|android, -fps) |
| Verificar tipo TSX | go tool nsgo check <dir> |
| Regenerar catálogos de mensajes | go tool nsgo i18n <dir> |
Las banderas anulan los valores de nsgo.yaml que nombran. build.targets y -target aceptan all, o nombres como windows_x64, macos_arm64, linux_x64, ios, web. build escribe un directorio por destino bajo -output (por defecto dist), nombrado <name>-<version>-<target>/. Un destino de prueba de escritorio se ejecuta solo en ese SO (-target windows en Windows, -target macos en una Mac); web, ios_simulator y android se ejecutan desde una Mac.
Las pruebas actúan como un usuario. nsgotest las ejecuta sin cabeza y sin ventana; -target ejecuta las mismas pruebas en una ventana real con entrada nativa y capturas de pantalla (app.Screenshot("nombre")).
func TestMain(m *testing.M) { nsgotest.Main(m) } // da a la plataforma el hilo principal
func TestPulsar(t *testing.T) {
app := nsgotest.Start(t, Raiz)
app.Press("text:Presióname")
app.Find("text:Presionado 1 veces")
}
Los selectores son los de Playwright: id:, label:, text: (una subcadena; text:"Guardar" o una cadena simple = todo el texto), role:<rol> <nombre>, view:<n>, /regexp/flags, y >> para descender. Pasos: Press, PressDown/PressUp, LongPress, Key, KeyPress, Type, Focus, Hover, Scroll, ScrollTo, Drag, Touch, Back, Resize, Settle, Advance, WaitFor, WaitForGone, Exists, Query, Texts, Dump. Lista completa: go doc github.com/oneo-me/nsgo/nsgotest.
Las aplicaciones TSX tienen la misma forma: App.tsx importa mount, signal, Text, View de "nsgo" (componentes de "nsgo/base") y termina con mount(App); main.go incrusta las fuentes y llama a nsgo.Run(compiler.App(src, "App.tsx")). Los archivos *.test.tsx se ejecutan como pruebas Go. Ver references/api.md.
Errores comunes
| Error | Realidad |
|---|---|
| Adivinar nombres de campos | NSGo es prelanzamiento con casi ninguna documentación pública, por lo que los identificadores recordados son incorrectos. go doc primero — un nombre incorrecto es un error de compilación |
Establecer tanto Content como ContentFunc | Pánico al montar. Elige uno |
NewSignal(cx, []string{...}) | T debe ser comparable. Usa NewSignalFunc con tu propio equal |
NewSignal fuera de un scope | Las señales necesitan un cx; el grafo es local a la gorutina, por lo que cx es el único manejador. Desde otra gorutina usa Scope.Post |
Esperar que Button.Label renderice | Label es el nombre accesible (lo que coinciden los selectores role:/label:). El texto visible viene de Children |
Importar internal/... | Las aplicaciones solo pueden importar nsgo, core, base, mobile, desktop, i18n, icons, tsx, nsgotest |
go build ./... en Windows o Linux | Falla: internal/platform/apple/edge.go tiene su etiqueta //go:build darwin perdida e importa purego/objc solo de darwin. Construye tu propio paquete (go build .), o añade la etiqueta en un checkout |
JustifyContent: core.AlignCenter | Dos enums: AlignItems toma core.AlignStart/Center/End/Stretch/Baseline, JustifyContent toma core.JustifyStart/Center/End/SpaceBetween/.... Mezclarlos es un error de compilación |
core.Shader{OnMouseMove: ...} | Un shader tiene OnPress, OnLayout, Style — sin manejador de movimiento del puntero. Pon OnMouseMove func(Point) / OnHoverOut func() en un core.View alrededor |
Confiar solo en MustCompileShader | Kage compila en tiempo de ejecución, por lo que un shader malo causa pánico al iniciar, no al construir. Prueba cada fuente con core.CompileShader e informa su error |
Un Scroll que nunca se desplaza y no muestra barra | Scroll por defecto tiene FlexShrink: 1, pero un padre View no: una columna con solo FlexGrow: 1 crece hasta su contenido, la vista de desplazamiento hereda esa altura y nada se desborda. Dale a la columna FlexGrow: 1, FlexShrink: 1 (el flex: 1 de React Native). Afírmalo: el marco del scroll debe ser más corto que la ventana |
| Buscar una barra de desplazamiento permanente | El indicador es una superposición: la plataforma nativa pinta un pulgar redondeado que se desvanece al desplazarse, se mantiene 700ms, luego se desvanece en 300ms; HideIndicator lo elimina. En la web es la barra propia del navegador (overflow-y: auto, scrollbar-width: auto) |
Una superposición fijada solo con Left | Una vista posicionada absolutamente con un borde horizontal toma el ancho restante de su bloque contenedor, no su ancho de contenido: un tooltip en Left: 34 dentro de un anclaje redondo de 26pt se queda sin nada y se envuelve a un carácter por línea. Dale un Width explícito |
| Alfa dentro de un color interpolado | AnimatedStyle.Background alimentado por core.Interpolate con Colors: {RGBA(0xffffff, 0.12), ...} pinta como blanco casi sólido, no 12%. Usa colores opacos para rellenos interpolados y mantén alfa para Style.Background |
MotionPreset.Name() como etiqueta | Devuelve camelCase (fadeIn, zoomIn, breathe), no el nombre de la constante Go |
| Una animación "vuelve al inicio" | Una señal o valor animado leído durante el renderizado, o NewAnimatedValue construido dentro del renderizado de un Dynamic: el tramo se reconstruye y los valores se reinician. Lee dentro |