Communitygithub.com

HycJack/nsgo

Use ao escrever, executar, testar, empacotar ou depurar um app NSGo. NSGo (github.com/oneo-me/nsgo) é um mecanismo de UI nativo multiplataforma com backend Go — a porta Go do Native Shell (NSH) — visando macOS, iOS, Windows, Linux, Android e web. Dispara ao criar um novo projeto NSGo, comandos `go tool nsgo`, nsgo.yaml, a API core/base (sinais, View, Text, Button, Style, classes Tailwind), animação e movimento (core.Motion, presets, AnimatedStyle, keyframes, easing, spring, movimento por shader, cenas 3D), apps TSX no goja, seletores nsgotest, incorporação ou empacotamento, ou código NSGo que não compila.

O que é nsgo?

O NSGo é um mecanismo de UI nativo multiplataforma escrito em Go. Ele permite que desenvolvedores criem aplicativos com lógica Go e componentes de UI que são renderizados usando visualizações nativas em cada plataforma (macOS, iOS, Windows, Linux, Android e web). Sua arquitetura usa um gráfico reativo orientado a sinais (Signals) e um sistema de animação baseado em dois clocks (commit e frame), suportando desde transições simples até animações complexas com shaders e cenas 3D. O NSGo suporta a criação de projetos com templates Go, TSX (compilado para goja) ou misto, e inclui um conjunto de ferramentas (`go tool nsgo`) para desenvolvimento, teste, empacotamento e incorporação em apps hospedeiros (XCFramework para iOS, AAR para Android, nsgo.js para web). Devido à sua natureza pré-lançamento e documentação pública limitada, é crucial consultar a documentação do módulo (`go doc`) antes de escrever código, pois nomes de campos incorretos resultam em erros de compilação.

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

Perguntar na sua IA favorita

Abre um novo chat com esta habilidade de agente já pré-carregada.

Documentação

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

SOAlvo nativoNotas
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:

ComandoEfeito
go tool nsgo init -name Contador .escreve no diretório atual, app chamado Contador
go tool nsgo init Contadorcria ./Contador/ contendo o app
go tool nsgo init -template tsx Contadorcria ./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.

TemplateArquivos
gonsgo.yaml, app.go, app_test.go
tsxnsgo.yaml, main.go, App.tsx, app.test.tsx, tsconfig.json, .gitignore
mixedo 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 com Scope.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. XFunc prevalece sobre X; 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.Func adapta 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 em Class ("flex-row gap-4 p-4 bg-white dark:bg-zinc-900"). Os componentes de base sã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.

NecessidadeUse
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ãocore.Motion + um core.MotionPreset (38 deles, todos honram reduce-motion)
Coreografia personalizada, sobreposição, esperas, trocasAnimatedStyle + gráfico AnimatedNode (Timing, Spring, Interpolate, Sequence, Stagger, Loop)
Por pixel / campo infinito / 3DUniforms de core.Shader como nós — zero commits
Desenho proceduralcore.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

TarefaComando
Executar, recompilar ao salvargo tool nsgo dev <dir>
Executar em um navegadorgo tool nsgo dev -target web <dir>
Executar no simulador/dispositivogo tool nsgo dev -target ios_simulator|android|ios <dir>
Rastrear cada commit... -trace
Pacotes de lançamentogo 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
Testesgo tool nsgo test <dir> (-run REGEXP, -junit FILE, -target macos|windows|linux|web|ios_simulator|android, -fps)
Verificar tipo TSXgo tool nsgo check <dir>
Regenerar catálogos de mensagensgo 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

ErroRealidade
Adivinhar nomes de camposNSGo é 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 ContentFuncPânico na montagem. Escolha um
NewSignal(cx, []string{...})T deve ser comparable. Use NewSignalFunc com seu próprio equal
NewSignal fora de um escopoSinais 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 renderizeLabel é 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 LinuxFalha: 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.AlignCenterDuas 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 sozinhoKage 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 barraScroll 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 permanenteO 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 apenasUma 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 interpoladaAnimatedStyle.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ótuloRetorna 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]

Habilidades Relacionadas