O que nsgo faz?
Um mecanismo de UI Go: a lógica do app, o gráfico reativo, estilos, layout e texto rodam em Go; cada plataforma desenha com suas próprias visualizações nativas. Apps são escritos em Go (ou TSX compilado para goja). Design, nomes e comportamento são portados do NSH (Rust).
Regra de ferro: leia a API, nunca adivinhe
NSGo é pré-lançamento, não tem API estável e quase nenhuma documentação pública na web, então identificadores lembrados estão quase sempre errados. Go falha ao compilar com um nome de campo errado. Antes de escrever um app, obtenha a verdade do próprio módulo:
go doc github.com/oneo-me/nsgo/core # ou: go doc -all ./core
go doc github.com/oneo-me/nsgo/base
Em um checkout do repositório, ARCHITECTURE.md, docs/architecture/*.md (25 documentos de assunto) e examples/ são autoritativos; examples/gallery exercita cada elemento, componente e serviço. Se você não tiver nem o módulo nem o repositório, diga isso — não substitua outro framework (Fyne, Gio, Wails) pelo pedido real do usuário.
Configuração
Requer Go 1.26.0+ (go.mod diz go 1.26.0).
| SO | Alvo nativo | Notas |
|---|---|---|
| macOS | ✅ | Toolchain completo: compila/assina cada alvo com Go + Xcode |
| Windows | ✅ | Compilação e execução nativa + web |
| Linux | ✅ | Precisa de GTK 4.14+, Pango, Cairo; WebKitGTK 6.0 e GStreamer para visualizações web/vídeo |
Adicione o módulo e sua ferramenta ao app uma vez — go tool nsgo então o executa sem instalar:
go get -tool github.com/oneo-me/nsgo/cmd/nsgo
NSGo não publica versões marcadas, então o módulo resolve para uma pseudo-versão (v0.0.0-2026…-46c7d9c). Alguns mirrors regionais retornam 404 em um módulo não marcado; se go get não conseguir encontrá-lo, fixe o proxy:
GOPROXY=https://proxy.golang.org,direct GOSUMDB=off go get -tool github.com/oneo-me/nsgo/cmd/nsgo
GOSUMDB=off é necessário apenas onde sum.golang.org está inacessível. Trabalhando a partir de um checkout local, aponte para ele e organize:
go mod edit -replace github.com/oneo-me/nsgo=/caminho/para/nsgo && go mod tidy
Criar um projeto
mkdir contador && cd contador
go mod init contador
go get -tool github.com/oneo-me/nsgo/cmd/nsgo # a biblioteca e a ferramenta
go tool nsgo init -name contador . # escreve nsgo.yaml, app.go, app_test.go
go mod tidy
go tool nsgo dev . # executa; recompila a cada salvamento
go tool nsgo test . # headless: 2 passed
init precisa de um nome. go tool nsgo init em um diretório existente falha com "." não é um nome de app, porque o nome padrão vem do diretório — e em . isso é .. Passe -name, ou passe um diretório para criar:
| Comando | Efeito |
|---|---|
go tool nsgo init -name Contador . | escreve no diretório atual, app chamado Contador |
go tool nsgo init Contador | cria ./Contador/ contendo o app |
go tool nsgo init -template tsx Contador | cria ./Contador/ como um app TSX |
init imprime seu próprio próximo passo (next: go tool nsgo dev Contador) — confie nele em vez da memória.
-template é um de go (padrão), tsx, mixed. O nome do app aceita letras, dígitos, espaços, - e _; o bundle id se torna dev.nsgo.<nome>. Passe -module para que go.mod também seja escrito.
| Template | Arquivos |
|---|---|
go | nsgo.yaml, app.go, app_test.go |
tsx | nsgo.yaml, main.go, App.tsx, app.test.tsx, tsconfig.json, .gitignore |
mixed | o mesmo, com App.tsx importando Go próprio e um native.go |
nsgo.yaml nomeia o app e seus alvos:
name: Ola
id: dev.oneo.nsgo.ola
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]
Também válido: build_number, appearance, theme, i18n, icon.{apple,png}, dev.icon, android.permissions, sign.{profile,keystore}, web.host. Ao lado, resources/ contém arquivos enviados (carregados como asset:), assets/ contém arquivos apenas do projeto (ícones), locales/ contém en.yaml, de.yaml… no formato ICU MessageFormat.
O app que você obtém (-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 {
contagem := 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("Pressionado %d vezes", contagem.Get()) }},
base.Button{
Label: "Pressione-me",
OnPress: func(core.PressEvent) { contagem.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: "Pressione-me", Style: core.Style{Color: core.RGB(0xffffff)}}},
},
}}
}
func main() { nsgo.Run(Root) }
nsgo.Run aceita uma visualização raiz (func(cx *core.Scope) core.Node) ou uma função de app (func(app *core.Scope)) que abre suas próprias janelas via desktop. Opções: WindowSize(l, a), Appearance(...), HideDock(), FPS(bool), WindowMaterial(...), Fonts(caminhos...), Component(nome, fabrica).
Modelo mental
- Sinais.
core.NewSignal[T comparable](cx, v)→.Get(),.Set(v),.Update(fn).NewMemo,NewEffect. O gráfico pertence à goroutine do mecanismo e nunca trava; outras goroutines entram comScope.Post. - Elementos são structs; o valor zero é o padrão (como
http.Server). Você define apenas o que deseja diferenciar. - Todo campo reativo tem um gêmeo:
Content+ContentFunc func() string,Style+StyleFunc,Class+ClassFunc.XFuncprevalece sobreX; definir ambos gera pânico na montagem. Campos sem forma estática aceitam apenas a função (Disabled func() bool,Selected,Expanded). - Componentes são qualquer tipo com
Render(cx *core.Scope) core.Node; seus campos são props.core.Funcadapta uma função simples. - Regiões —
Show,Each,Dynamic,Catch— reconstroem apenas seu próprio intervalo.Eaché uma função genérica, não um método. - Estilização: valores tipados
core.Style, ou classes Tailwind emClass("flex-row gap-4 p-4 bg-white dark:bg-zinc-900"). Os componentes debasesão deliberadamente sem estilo — apenas comportamento, estado e acessibilidade.
Movimento
Dois relógios. Commit = Go re-renderiza um intervalo (sinais, Show/Each/Dynamic). Frame = o animador da plataforma avança campos AnimatedStyle e uniforms de shader, sem commit. Todo movimento pertence ao relógio de frame; um valor lido durante a renderização re-renderiza a subárvore e reinicia toda animação nela.
| Necessidade | Use |
|---|---|
| Uma mudança de estado (hover, press, seleção, redimensionamento) | Style.Transition — uma duração, core.EaseOut, e uma máscara de bit Properties; nenhum gráfico e ignorado sob reduce-motion |
| Entrada/saída/atenção/loop padrão | core.Motion + um core.MotionPreset (38 deles, todos honram reduce-motion) |
| Coreografia personalizada, sobreposição, esperas, trocas | AnimatedStyle + gráfico AnimatedNode (Timing, Spring, Interpolate, Sequence, Stagger, Loop) |
| Por pixel / campo infinito / 3D | Uniforms de core.Shader como nós — zero commits |
| Desenho procedural | core.Canvas Draw (re-grava por mudança) ou d.Bind (a plataforma o repete, sem re-gravação) |
spec := core.FadeInUp.Spec() // ou seu próprio core.MotionSpec
m := core.NewMotion(cx, &spec) // nil = constrói em repouso, Play depois
return core.View{Style: core.Style{Animated: m.Style()}, Children: []core.Node{card()}}
Cinco regras que valem mais que a API: (1) nunca leia um sinal ou valor animado na renderização — apenas closures ou AnimatedStyle; (2) re-estilize em vez de reconstruir; (3) uma animação finita mantém seu último valor, então o frame no final deve ser o legível (um loop que retorna ao início parece em branco quando se estabiliza); (4) um elemento animado precisa de um pai medido — uma View totalmente absoluta mede 0×0 e corta tudo; (5) campos de AnimatedStyle são AnimatedNodes, não números (core.Constant(1), não 1), e não tem Width/Height/Perspective.
Leia references/animation.md antes de escrever qualquer animação: tabela de presets, números de easing/duração que funcionaram, receitas de coreografia, movimento com shader e canvas, 3D (ray marching), testes e uma tabela de sintoma→causa para bugs de movimento.
Loop de desenvolvimento, compilação, teste
| Tarefa | Comando |
|---|---|
| Executar, recompilar ao salvar | go tool nsgo dev <dir> |
| Executar em um navegador | go tool nsgo dev -target web <dir> |
| Executar no simulador/dispositivo | go tool nsgo dev -target ios_simulator|android|ios <dir> |
| Rastrear cada commit | ... -trace |
| Pacotes de lançamento | go tool nsgo build <dir> (-target all, -output DIR, -run, -device D) |
| Incorporar em um app hospedeiro | ... build -embed → XCFramework (iOS) / AAR (Android) / nsgo.js (web); desktops usam nsgo.NewView |
| Testes | 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 mensagens | go tool nsgo i18n <dir> |
Flags substituem os valores de nsgo.yaml que nomeiam. build.targets e -target aceitam all, ou nomes como windows_x64, macos_arm64, linux_x64, ios, web. build escreve um diretório por alvo sob -output (padrão dist), nomeado <nome>-<versão>-<alvo>/. Um alvo de teste desktop roda apenas naquele SO (-target windows no Windows, -target macos em um Mac); web, ios_simulator e android rodam a partir de um Mac.
Testes agem como um usuário. nsgotest os executa headless sem janela; -target executa os mesmos testes em uma janela real com entrada nativa e capturas de tela (app.Screenshot("nome")).
func TestMain(m *testing.M) { nsgotest.Main(m) } // dá à plataforma a thread principal
func TestPress(t *testing.T) {
app := nsgotest.Start(t, Root)
app.Press("text:Pressione-me")
app.Find("text:Pressionado 1 vezes")
}
Seletores são do Playwright: id:, label:, text: (uma substring; text:"Salvar" ou uma string simples = texto completo), role:<papel> <nome>, view:<n>, /regexp/flags, e >> para descer. Passos: 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.
Apps TSX têm a mesma forma: App.tsx importa mount, signal, Text, View de "nsgo" (componentes de "nsgo/base") e termina com mount(App); main.go incorpora as fontes e chama nsgo.Run(compiler.App(src, "App.tsx")). Arquivos *.test.tsx rodam como testes Go. Veja references/api.md.
Erros comuns
| Erro | Realidade |
|---|---|
| Adivinhar nomes de campos | NSGo é pré-lançamento com quase nenhuma documentação pública, então identificadores lembrados estão errados. go doc primeiro — um nome errado é um erro de compilação |
Definir ambos Content e ContentFunc | Pânico na montagem. Escolha um |
NewSignal(cx, []string{...}) | T deve ser comparable. Use NewSignalFunc com seu próprio equal |
NewSignal fora de um escopo | Sinais precisam de um cx; o gráfico é local à goroutine, então cx é o único manipulador. De outra goroutine use Scope.Post |
Esperar que Button.Label renderize | Label é o nome acessível (o que seletores role:/label: correspondem). Texto visível vem de Children |
Importar internal/... | Apps podem importar apenas nsgo, core, base, mobile, desktop, i18n, icons, tsx, nsgotest |
go build ./... no Windows ou Linux | Falha: internal/platform/apple/edge.go está faltando sua tag //go:build darwin e importa purego/objc apenas para darwin. Compile seu próprio pacote (go build .), ou adicione a tag em um checkout |
JustifyContent: core.AlignCenter | Duas enumerações: AlignItems aceita core.AlignStart/Center/End/Stretch/Baseline, JustifyContent aceita core.JustifyStart/Center/End/SpaceBetween/.... Misturá-las é um erro de compilação |
core.Shader{OnMouseMove: ...} | Um shader tem OnPress, OnLayout, Style — nenhum manipulador de movimento do ponteiro. Coloque OnMouseMove func(Point) / OnHoverOut func() em um core.View ao redor dele |
Confiar em MustCompileShader sozinho | Kage compila em tempo de execução, então um shader ruim causa pânico na inicialização, não na compilação. Teste cada fonte com core.CompileShader e relate seu erro |
Um Scroll que nunca rola e não mostra barra | Scroll padrão é FlexShrink: 1, mas um pai View não: uma coluna com apenas FlexGrow: 1 cresce até seu conteúdo, a visualização de scroll herda essa altura e nada transborda. Dê à coluna FlexGrow: 1, FlexShrink: 1 (o flex: 1 do React Native). Afirme: o quadro do scroll deve ser mais curto que a janela |
| Procurar por uma barra de rolagem permanente | O indicador é uma sobreposição: nativo pinta um polegar arredondado que aparece enquanto rola, segura por 700ms, depois desaparece em 300ms; HideIndicator o remove. Na web é a própria barra do navegador (overflow-y: auto, scrollbar-width: auto) |
Uma sobreposição fixada com Left apenas | Uma visualização posicionada absolutamente com uma borda horizontal pega a largura restante de seu bloco contido, não sua largura de conteúdo: uma dica em Left: 34 dentro de uma âncora redonda de 26pt fica com nada e quebra para um caractere por linha. Dê a ela uma Width explícita |
| Alfa dentro de uma cor interpolada | AnimatedStyle.Background alimentado por core.Interpolate com Colors: {RGBA(0xffffff, 0.12), ...} pinta como branco quase sólido, não 12%. Use cores opacas para preenchimentos interpolados e mantenha alfa para Style.Background |
MotionPreset.Name() como rótulo | Retorna camelCase (fadeIn, zoomIn, breathe), não o nome da constante Go |
| Uma animação "volta ao início" | Um sinal ou valor animado lido durante a renderização, ou NewAnimatedValue construído dentro da renderização de um Dynamic: o intervalo se reconstrói e os valores reiniciam. Leia dentr... |
[truncado para tradução]