Communitygithub.com

HycJack/nsgo

用於編寫、執行、測試、打包或除錯 NSGo 應用程式。NSGo 是一個基於 Go 後端的跨平台原生 UI 引擎,支援 macOS、iOS、Windows、Linux、Android 和網頁。

nsgo 是什麼?

NSGo 是一個基於 Go 語言的跨平台原生 UI 引擎,將 Native Shell (NSH) 的概念從 Rust 移植到 Go。其核心架構是:應用邏輯、反應式圖形、樣式、佈局和文字運行在 Go 中,而每個平台則使用其自身的原生視圖進行繪製。這使得開發者能夠使用常見的 Go 語言工具鏈來建立原生使用者介面,並支援從桌面到行動裝置和網頁的多種目標平台。 此技能提供了開發 NSGo 應用程式的基本指引,包括專案初始化、開發循環、測試和發布。它強調了一個重要鐵律:由於 NSGo 處於預發布階段且 API 不穩定,在編寫程式碼前必須使用 `go doc` 查閱模組本身的 API 文件,而非依賴記憶猜測,因為一個錯誤的欄位名稱就會導致編譯失敗。技能涵蓋了訊號(Signal)、元件的反應式模型、動畫系統(包含幀時脈驅動的 AnimatedStyle 和 MotionPreset)、以及透過 `nsgotest` 進行的測試。 此技能支援的工具平台包括 Claude Code、Codex 和 Cursor。它可以透過觸發特定指令或檔案模式(例如建立新 NSGo 專案、`go tool nsgo` 命令、`nsgo.yaml` 配置檔、或處理無法編譯的 NSGo 程式碼)來自動啟用。

相容平台~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✅原生和網頁建構與執行
Linux✅需要 GTK 4.14+、Pango、Cairo;用於網頁檢視/影片的 WebKitGTK 6.0 和 GStreamer

將模組及其工具新增到應用程式一次 — 然後 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 個測試通過

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。應用程式名稱可以包含字母、數字、空格、- 和 _;套件識別碼會變成 dev.nsgo.<名稱>。傳遞 -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/ 存放了使用 ICU MessageFormat 的 en.yaml、de.yaml…。

您獲得的應用程式(-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)。

心智模型

  • 訊號。 core.NewSignal[T comparable](cx, v) → .Get()、.Set(v)、.Update(fn)。NewMemo、NewEffect。圖形屬於引擎 goroutine 且永不鎖定;其他 goroutine 使用 Scope.Post 進入它。
  • 元素是結構體;零值即為預設值(類似於 http.Server)。您只設定想要不同的部分。
  • 每個反應式欄位都有一個孿生體:Content + ContentFunc func() string、Style + StyleFunc、Class + ClassFunc。XFunc 優於 X;同時設定兩者會在掛載時引發恐慌。沒有靜態形式的欄位只接受函式(Disabled func() bool、Selected、Expanded)。
  • 元件是任何具有 Render(cx *core.Scope) core.Node 方法的型別;其欄位即為 props。core.Func 適用於純函式。
  • 區域 — Show、Each、Dynamic、Catch — 僅重建其自身的跨度。Each 是一個泛型函式,而不是一個方法。
  • 樣式設定:型別化的 core.Style 值,或在 Class 中使用 Tailwind 類別("flex-row gap-4 p-4 bg-white dark:bg-zinc-900")。base 的元件故意不設定樣式 — 僅提供行為、狀態和無障礙功能。

動畫

兩個時脈。Commit = Go 重新渲染一個跨度(訊號、Show/Each/Dynamic)。Frame = 平台的動畫器驅動 AnimatedStyle 欄位和著色器統一變數,不進行 commit。所有動畫都屬於幀時脈;在渲染期間讀取的值會重新渲染子樹並重新啟動其中的每個動畫。

需要使用
狀態變更(懸停、按壓、選取、調整大小)Style.Transition — 一個持續時間、core.EaseOut 和一個 Properties 位元遮罩;完全不需要圖形,且在減少動畫模式下會被跳過
標準的進場/出場/吸引/循環core.Motion + 一個 core.MotionPreset(共 38 個,全部都會遵循減少動畫模式)
自訂編排、重疊、保持、交換AnimatedStyle + AnimatedNode 圖形(Timing、Spring、Interpolate、Sequence、Stagger、Loop)
逐畫素 / 無限場域 / 3Dcore.Shader 統一變數作為節點 — 零次 commit
程式化繪圖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 欄位是 AnimatedNodes,不是數字(core.Constant(1),而不是 1),而且它沒有 Width/Height/Perspective。

在編寫任何動畫之前,請閱讀 references/animation.md:預設表格、已確認有效的緩動/持續時間數值、編排食譜、著色器和畫布動畫、3D(光線行進)、測試,以及動畫錯誤的症狀→原因表。

開發循環、建構、測試

任務指令
執行,儲存時重新建構go tool nsgo dev <dir>
在瀏覽器中執行go tool nsgo dev -target web <dir>
在模擬器/裝置上執行go tool nsgo dev -target ios_simulator|android|ios <dir>
追蹤每次 commit... -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|windows|linux|web|ios_simulator|android、-fps)
型別檢查 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>/。桌面測試目標僅在其對應的 OS 上執行(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在掛載時引發恐慌。選擇其中一個
NewSignal(cx, []string{...})T 必須是 comparable。使用帶有您自己的 equal 的 NewSignalFunc
在 scope 外部使用 NewSignal訊號需要一個 cx;圖形是 goroutine 本地的,因此 cx 是唯一的控制代碼。從另一個 goroutine 使用 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 在執行時編譯,因此錯誤的著色器會在啟動時引發恐慌,而不是在建構時。使用 core.CompileShader 測試每個原始碼並報告其錯誤
一個永不滾動且不顯示條的 ScrollScroll 預設為 FlexShrink: 1,但 View 父元素不會:只有 FlexGrow: 1 的欄位會增長到其內容高度,滾動視圖繼承該高度,因此沒有任何內容溢出。為該欄位同時設定 FlexGrow: 1, FlexShrink: 1(React Native 的 flex: 1)。斷言:滾動視圖的框架必須比視窗短
尋找一個永久捲軸指示器是一個疊加層:原生會繪製一個在滾動時淡入、保持 700 毫秒、然後在 300 毫秒內淡出的圓角滑塊;HideIndicator 會移除它。在網頁上,它是瀏覽器自身的條(overflow-y: auto、scrollbar-width: auto)
一個僅使用 Left 固定位置的疊加層一個具有一個水平邊的絕對定位檢視會占用其包含區塊的 剩餘 寬度,而不是其內容寬度:一個在 26pt 圓形錨點內設定 Left: 34 的工具提示最終沒有剩餘空間,並會折疊成每行一個字元。為它指定一個明確的 Width
在插值顏色內使用 Alpha由 core.Interpolate 提供、並使用 Colors: {RGBA(0xffffff, 0.12), ...} 的 AnimatedStyle.Background 會繪製成接近實心的白色,而不是 12%。對插值填充使用不透明顏色,並將 Alpha 保留給 Style.Background
MotionPreset.Name() 作為標籤回傳駝峰式命名(fadeIn、zoomIn、breathe),而不是 Go 常數的名稱
動畫「跳回起點」在渲染期間讀取了訊號或動畫值,或在 Dynamic 的渲染內部建構了 NewAnimatedValue:跨度會重建,值會重新啟動。在渲染內部讀取…

…

[內容已截斷]

相關技能