Communitygithub.com

HycJack/nsgo

编写、运行、测试、打包或调试 NSGo 应用时使用。NSGo 是一个使用 Go 后端的跨平台原生 UI 引擎。

nsgo 是什么?

NSGo 是一个跨平台原生 UI 引擎,它是 Rust 原生框架 Native Shell(NSH)的 Go 移植版本。它允许开发者使用 Go 语言(或通过 goja 编译的 TSX 代码)编写应用逻辑,而每个平台则使用自身的原生视图进行绘制。该工具支持 macOS、iOS、Windows、Linux、Android 和 Web 平台。 NSGo 的核心在于其 **反应式编程模型**,其中信号(Signals)、记忆(Memos)和效果(Effects)构成了一个自动依赖追踪的响应式图。UI 元素被定义为结构体,所有字段都有函数形式的变体(如 `Content` 与 `ContentFunc`),以实现动态更新。其动效系统使用独立的帧时钟来驱动 `AnimatedStyle` 属性和着色器统一变量,避免触发响应式组件树的全面重渲染。 开发支持非常完善:通过 `go tool nsgo dev` 实现热重载,使用 `nsgotest` 进行无头或原生 UI 测试,并通过 `go tool nsgo build` 打包为各平台原生格式(macOS 的 .app、iOS 的 .ipa、Android 的 .apk、Web 的静态文件)。测试选择器采用 Playwright 语法,支持基于文本、标签、角色和正则表达式的定位。 一个关键的设计约束是,NSGo 目前是预发布版本,没有稳定 API,且公开文档极少。**开发者必须通过 `go doc` 直接查询模块来确认标识符的正确性,而不是凭猜测**,因为错误的字段名会导致编译错误。此外,NSGo 的所有文档(包括 `ARCHITECTURE.md` 和示例代码)都应被视作权威参考。

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

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

nsgo 是做什么的?

一个 Go UI 引擎:应用逻辑、响应式图、样式、布局和文本均在 Go 中运行;每个平台使用自身的原生视图进行绘制。应用可使用 Go 或编译为 goja 的 TSX 编写。设计、命名和行为均移植自 NSH(Rust)。

铁律:阅读 API,切勿猜测

NSGo 是预发布版本,没有稳定 API,且网络上几乎没有公开文档,因此回忆的标识符几乎总是错误的。Go 在遇到一个错误的字段名时就会编译失败。在编写应用之前,请从模块本身获取真相:

go doc github.com/oneo-me/nsgo/core      # 或:go doc -all ./core
go doc github.com/oneo-me/nsgo/base

在仓库的检出目录中,ARCHITECTURE.md、docs/architecture/*.md(25 个主题文档)和 examples/ 是权威的;examples/gallery 练习了每个元素、组件和服务。如果你既没有模块也没有仓库,请说明情况 — 不要用其他框架(Fyne、Gio、Wails)来替代用户的实际请求。

设置

需要 Go 1.26.0+(go.mod 中声明为 go 1.26.0)。

操作系统原生目标备注
macOS✅完整工具链:使用 Go + Xcode 构建/签名每个目标
Windows✅原生 + Web 构建和运行
Linux✅需要 GTK 4.14+、Pango、Cairo;WebKitGTK 6.0 和 GStreamer 用于 Web 视图/视频

只需向应用添加一次模块及其工具 — go tool nsgo 随后无需安装即可运行:

go get -tool github.com/oneo-me/nsgo/cmd/nsgo

NSGo 不发布任何带标签的版本,因此模块解析为一个伪版本(v0.0.0-2026…-46c7d9c)。某些地区镜像对未打标签的模块会返回 404;如果 go get 找不到,请固定代理:

GOPROXY=https://proxy.golang.org,direct GOSUMDB=off go get -tool github.com/oneo-me/nsgo/cmd/nsgo

只有在 sum.golang.org 不可达时才需要 GOSUMDB=off。如果使用本地检出,请指向它并整理:

go mod edit -replace github.com/oneo-me/nsgo=/path/to/nsgo && go mod tidy

创建项目

mkdir counter && cd counter
go mod init counter
go get -tool github.com/oneo-me/nsgo/cmd/nsgo   # 库和工具
go tool nsgo init -name counter .              # 写入 nsgo.yaml, app.go, app_test.go
go mod tidy
go tool nsgo dev .                             # 运行;每次保存时重建
go tool nsgo test .                            # 无头模式:2 passed

init 需要一个名称。 在已有目录中直接运行 go tool nsgo init 会失败,报错 "." is not an app name,因为默认名称来自目录 — 而 . 就是 .。请传入 -name,或者在要创建的目录中执行:

命令效果
go tool nsgo init -name Counter .写入当前目录,应用名为 Counter
go tool nsgo init Counter创建 ./Counter/ 目录,其中包含应用
go tool nsgo init -template tsx Counter创建 ./Counter/ 作为 TSX 应用

init 会打印下一步(next: go tool nsgo dev Counter)— 请信任它而非记忆。

-template 可以是 go(默认)、tsx、mixed。应用名称可以包含字母、数字、空格、- 和 _;bundle id 变为 dev.nsgo.<name>。传入 -module 也会写入 go.mod。

模板文件
gonsgo.yaml, app.go, app_test.go
tsxnsgo.yaml, main.go, App.tsx, app.test.tsx, tsconfig.json, .gitignore
mixed同上,且 App.tsx 导入自己的 Go 代码以及一个 native.go

nsgo.yaml 指定应用名称及其目标:

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]

其他有效字段:build_number, appearance, theme, i18n, icon.{apple,png}, dev.icon, android.permissions, sign.{profile,keystore}, web.host。此外,resources/ 存放发布文件(加载为 asset:),assets/ 存放仅项目文件(图标),locales/ 存放 en.yaml, de.yaml… 采用 ICU MessageFormat。

你得到的应用(-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 接收一个根视图(func(cx *core.Scope) core.Node)或一个应用函数(func(app *core.Scope)),后者通过 desktop 打开自己的窗口。可选参数:WindowSize(w, h), Appearance(...), HideDock(), FPS(bool), WindowMaterial(...), Fonts(paths...), Component(name, factory)。

心智模型

  • 信号(Signals). core.NewSignal[T comparable](cx, v) → .Get(), .Set(v), .Update(fn)。NewMemo, NewEffect。响应式图属于引擎协程,从不加锁;其他协程通过 Scope.Post 进入该图。
  • 元素是结构体;零值是默认值(类似 http.Server)。你只需设置想要改变的部分。
  • 每个响应式字段都有一对双胞胎:Content + ContentFunc func() string, Style + StyleFunc, Class + ClassFunc。XFunc 优先于 X;同时设置两者会在挂载时 panic。没有静态形式的字段只接受函数(Disabled func() bool, Selected, Expanded)。
  • 组件是任何实现了 Render(cx *core.Scope) core.Node 的类型;其字段就是 props。core.Func 适配一个普通函数。
  • 区域(Regions) — Show, Each, Dynamic, Catch — 只重建自己的范围。Each 是一个泛型函数,而非方法。
  • 样式:类型化的 core.Style 值,或在 Class 中使用 Tailwind 类("flex-row gap-4 p-4 bg-white dark:bg-zinc-900")。base 的组件故意不带样式 — 只包含行为、状态和可访问性。

动效

两种时钟。提交 = Go 重渲染一个区域(信号,Show/Each/Dynamic)。帧 = 平台的动画器运行 AnimatedStyle 字段和着色器统一变量,不触发提交。所有动效应属于帧时钟;在渲染期间读取的值会重渲染子树并重启其中的所有动画。

需求使用
状态变化(悬停、按下、选中、调整大小)Style.Transition — 一个时长,core.EaseOut,以及一个 Properties 位掩码;完全没有响应式图,且在减少动效时跳过
标准入场/退场/注意/循环core.Motion + 一个 core.MotionPreset(共 38 个,都尊重减少动效)
自定义编排、重叠、保持、交换AnimatedStyle + AnimatedNode 图(Timing, Spring, Interpolate, Sequence, Stagger, Loop)
逐像素 / 无限场 / 3Dcore.Shader 统一变量作为节点 — 零次提交
程序化绘制core.Canvas Draw(每次变化重新记录)或 d.Bind(平台重放,不重新记录)
spec := core.FadeInUp.Spec()             // 或你自己的 core.MotionSpec
m := core.NewMotion(cx, &spec)           // nil = 在静止时构建,稍后 Play
return core.View{Style: core.Style{Animated: m.Style()}, Children: []core.Node{card()}}

五条比 API 更重要的规则:(1) 永远不要在渲染中读取信号或动画值 — 只使用闭包或 AnimatedStyle;(2) 重新设置样式而非重建;(3) 有限动画 保持 其最后一个值,因此结束时的帧必须是可读的(如果循环回到起点,一旦稳定,看起来就是空白);(4) 动画元素需要一个已测量的父元素 — 全绝对定位的 View 尺寸为 0×0 并裁剪所有内容;(5) AnimatedStyle 字段是 AnimatedNode,而不是数字(core.Constant(1),而不是 1),并且它没有 Width/Height/Perspective。

在编写任何动画之前,阅读 references/animation.md:预设表格、有效的缓动/时长值、编排方案、着色器和画布动效、3D(光线步进)、测试以及动效 bug 的症状→原因表。

开发循环、构建、测试

任务命令
运行,保存时重建go tool nsgo dev <dir>
在浏览器中运行go tool nsgo dev -target web <dir>
在模拟器/设备上运行`go tool nsgo dev -target ios_simulator
跟踪每次提交... -trace
发布包go tool nsgo build <dir>(-target all, -output DIR, -run, -device D)
嵌入宿主应用... build -embed → XCFramework(iOS)/ AAR(Android)/ nsgo.js(Web);桌面使用 nsgo.NewView
测试go tool nsgo test <dir>(-run REGEXP, -junit FILE, `-target macos
类型检查 TSXgo tool nsgo check <dir>
重新生成消息目录go tool nsgo i18n <dir>

标志会覆盖它们所命名的 nsgo.yaml 值。build.targets 和 -target 接受 all,或类似 windows_x64, macos_arm64, linux_x64, ios, web 的名称。build 在每个目标下写入一个目录,位于 -output(默认 dist)下,命名为 <name>-<version>-<target>/。桌面测试目标仅在该操作系统上运行(Windows 上使用 -target windows,Mac 上使用 -target macos);web, ios_simulator 和 android 从 Mac 运行。

测试的行为类似于用户。nsgotest 以无头模式运行,不显示窗口;-target 在真实窗口中运行相同的测试,使用原生输入和截图(app.Screenshot("name"))。

func TestMain(m *testing.M) { nsgotest.Main(m) }   // 将主线程交给平台

func TestPress(t *testing.T) {
	app := nsgotest.Start(t, Root)
	app.Press("text:Press me")
	app.Find("text:Pressed 1 times")
}

选择器采用 Playwright 语法:id:, label:, text:(子字符串;text:"Save" 或裸字符串 = 完整文本),role:<role> <name>, view:<n>, /regexp/flags,以及 >> 用于下降。步骤:Press, PressDown/PressUp, LongPress, Key, KeyPress, Type, Focus, Hover, Scroll, ScrollTo, Drag, Touch, Back, Resize, Settle, Advance, WaitFor, WaitForGone, Exists, Query, Texts, Dump。完整列表:go doc github.com/oneo-me/nsgo/nsgotest。

TSX 应用具有相同的结构:App.tsx 从 "nsgo" 导入 mount, signal, Text, View(组件从 "nsgo/base" 导入),并以 mount(App) 结尾;main.go 嵌入源文件并调用 nsgo.Run(compiler.App(src, "App.tsx"))。*.test.tsx 文件作为 Go 测试运行。请参阅 references/api.md。

常见错误

错误现实
猜测字段名NSGo 是预发布版本,几乎没有公开文档,因此回忆的标识符是错误的。先运行 go doc — 一个错误名称就会导致编译错误
同时设置 Content 和 ContentFunc挂载时 panic。选择其一
NewSignal(cx, []string{...})T 必须是 comparable。使用 NewSignalFunc 配合你自己的 equal
在作用域外使用 NewSignal信号需要一个 cx;响应式图是协程本地的,因此 cx 是唯一的句柄。从其他协程使用 Scope.Post
期望 Button.Label 渲染Label 是可访问的名称(role:/label: 选择器匹配的内容)。可见文本来自 Children
导入 internal/...应用只能导入 nsgo, core, base, mobile, desktop, i18n, icons, tsx, nsgotest
在 Windows 或 Linux 上运行 go build ./...失败:internal/platform/apple/edge.go 缺少其 //go:build darwin 标签,并导入了仅 darwin 可用的 purego/objc。构建你自己的包(go build .),或者在检出中添加标签
JustifyContent: core.AlignCenter两个枚举:AlignItems 接受 core.AlignStart/Center/End/Stretch/Baseline,JustifyContent 接受 core.JustifyStart/Center/End/SpaceBetween/...。混用会导致编译错误
core.Shader{OnMouseMove: ...}着色器有 OnPress, OnLayout, Style — 没有指针移动处理器。将 OnMouseMove func(Point) / OnHoverOut func() 放在包裹它的 core.View 上
单独信任 MustCompileShaderKage 在运行时编译,因此糟糕的着色器会在启动时 panic,而不是在构建时。使用 core.CompileShader 测试每个源并报告其错误
从不滚动且不显示滚动条的 ScrollScroll 默认为 FlexShrink: 1,但 View 父容器并非如此:只有 FlexGrow: 1 的列会增长到其内容高度,滚动视图继承该高度,因此没有溢出。给该列添加 FlexGrow: 1, FlexShrink: 1(React Native 的 flex: 1)。断言它:滚动视图的框架必须短于窗口
寻找永久滚动条指示器是一个叠加层:原生绘制一个圆角拇指,在滚动时淡入,保持 700ms,然后在 300ms 内淡出;HideIndicator 会移除它。在 Web 上,它是浏览器自身的滚动条(overflow-y: auto, scrollbar-width: auto)
仅使用 Left 固定的叠加层具有一个水平边缘的绝对定位视图会占据其包含块剩余的宽度,而不是其内容宽度:在一个 26pt 的圆形锚点内,Left: 34 的工具提示只剩下很少的空间,并换行到每行一个字符。给它一个显式的 Width
插值颜色内的 Alpha由 core.Interpolate 提供的 AnimatedStyle.Background,如果使用 Colors: {RGBA(0xffffff, 0.12), ...},则会绘制成接近纯白色,而不是 12%。为插值填充使用不透明颜色,并为 Style.Background 保留 alpha
MotionPreset.Name() 作为标签返回 camelCase(fadeIn, zoomIn, breathe),而不是 Go 常量的名称
动画“跳回起点”在渲染期间读取了信号或动画值,或在 Dynamic 的渲染内部构建了 NewAnimatedValue:范围会重建,并且值会重启。请在 […] 内部阅读

[…](内容已截断)

相关技能