# NSGo documentation --- Source: https://nsgo.dev/docs/index.md # Introduction > What NSGo is, what it runs on and how it works. NSGo is a cross-platform native UI framework written in Go. You write an app once, in Go or in TSX, and it runs on iOS and its App Clips, tvOS, watchOS, Android, macOS, Windows, Linux and the web, drawn by each platform's own views. Go apps also run in WeChat, QQ, Alipay and Douyin mini programs, experimentally: the platforms hold an app to 2 MB, which larger apps exceed. ## What you write An app is a Go main package. Its components return elements: views, text, images, inputs and the controls in `ui`. State lives in signals, and only what reads a signal updates when it changes. ```go func Root(cx *core.Scope) core.Node { count := core.NewSignal(cx, 0) return core.View{Style: core.Style{Padding: core.All(24), Gap: 12}, Children: []core.Node{ core.Text{ContentFunc: func() string { return fmt.Sprintf("Pressed %d times", count.Get()) }}, ui.Button{Text: "Press me", OnPress: func() { count.Update(func(n int) int { return n + 1 }) }}, }} } ``` The same app can be written in TSX and styled with Tailwind classes ([TSX apps](https://nsgo.dev/docs/tsx.md)). ## How it works A Go engine owns the app: the reactive graph, styles, flexbox and grid layout, and text preparation. Each update produces one batch of changes, and a thin backend applies it to the platform's native views. Animation runs on the platform's frame clock, so nothing commits once a frame. | Platform | Draws with | | --- | --- | | iOS | UIKit and CoreText | | Android | Android Views | | macOS | AppKit and CoreText | | Windows | Direct2D and DirectWrite | | Linux | GTK 4, Pango and Cairo | | Web | WebAssembly and DOM elements | Every target builds on a Mac with Go; iOS also needs Xcode. The `nsgo` tool creates, runs, tests and packages apps. ## Status NSGo is early: it has no stable API or release yet, and what is missing is in the [roadmap](https://github.com/nsgo-dev/nsgo/blob/main/docs/roadmap.md). It ports the design of [Native Shell](https://github.com/longbridge/nsh) to Go. ## How to read the docs Every page answers one question. A code example is a fragment of a component unless the page says otherwise: `cx` is the `*core.Scope` a component receives. Apps use `ui`'s components, which have a look; `base` holds the same controls without a look, for authors of components. - **Get started**: [setup](https://nsgo.dev/docs/getting-started.md), then example apps built step by step, [a todo app](https://nsgo.dev/docs/tutorials/todo.md), [a notes app](https://nsgo.dev/docs/tutorials/notes.md) and [popular GitHub repositories](https://nsgo.dev/docs/tutorials/repos.md). - **Toolchain**: [running and debugging](https://nsgo.dev/docs/debugging.md), [testing](https://nsgo.dev/docs/testing.md), [building and shipping](https://nsgo.dev/docs/building.md) and [embedding](https://nsgo.dev/docs/embedding.md). - **Components**: [the most-used components](https://nsgo.dev/docs/ui.md), their variants, states and accessibility. - **Core concepts**: [components and state](https://nsgo.dev/docs/components.md), [layout](https://nsgo.dev/docs/layout.md), [styles](https://nsgo.dev/docs/styles.md), [text](https://nsgo.dev/docs/text.md), [lists](https://nsgo.dev/docs/lists.md), [navigation](https://nsgo.dev/docs/navigation.md), [forms](https://nsgo.dev/docs/forms.md), [localization](https://nsgo.dev/docs/localization.md), [TSX](https://nsgo.dev/docs/tsx.md) and the rest. - **Platform features**: [platform services](https://nsgo.dev/docs/services.md), [notifications](https://nsgo.dev/docs/notifications.md), [haptics](https://nsgo.dev/docs/haptics.md), [widgets](https://nsgo.dev/docs/widgets.md), [Live Activities](https://nsgo.dev/docs/live-activities.md) and [permissions](https://nsgo.dev/docs/permissions.md). - **Writing components**: [components for other people's apps](https://nsgo.dev/docs/authoring.md), unstyled and styled. - **Contributing**: [build NSGo from source](https://nsgo.dev/docs/contributing.md), find your way around it and send a change. - The [live demo](https://nsgo.dev/gallery/): every component, in your browser. --- Source: https://nsgo.dev/docs/comparison.md # Compared with other frameworks > Where NSGo fits beside NSH, MyGo and other UI frameworks, and what it gives up. NSGo's place among UI frameworks comes from three choices: apps are written in Go (or TSX on top of it), each platform draws with its own views, and one codebase covers phones, desktops and the web. Other frameworks make other choices, and each choice costs something. ## At a glance | Framework | Language | Draws with | Targets | | --- | --- | --- | --- | | NSGo | Go, TSX | Each platform's views | iOS, App Clips, tvOS, watchOS, Android, macOS, Windows, Linux, web; mini programs, experimentally | | [NSH](https://nsh.dev) | Rust, TSX | Each platform's views | iOS, Android, macOS, Windows, Linux, web, GPUI apps, terminals; HarmonyOS and WeChat mini programs through community platforms | | [MyGo](https://github.com/egoist/mygo) | Go, web pages | Its own GPU renderer, or the system webview | macOS, Windows, Linux, web | | [Fyne](https://fyne.io), [Gio](https://gioui.org) | Go | Their own renderer | Desktops, phones, web | | [Wails](https://wails.io) | Go, web pages | The system webview | Desktops | | [Flutter](https://flutter.dev) | Dart | Its own renderer | Phones, desktops, web | | [Compose Multiplatform](https://www.jetbrains.com/compose-multiplatform/) | Kotlin | Its own renderer; Android's Compose | Phones, desktops, web | | [React Native](https://reactnative.dev) | JavaScript, TSX | Each platform's views | iOS, Android; others through community platforms | | [.NET MAUI](https://dotnet.microsoft.com/apps/maui) | C# | Each platform's controls | iOS, Android, macOS, Windows | | [Avalonia](https://avaloniaui.net) | C# | Its own renderer | Desktops, phones, web | | [Qt](https://www.qt.io) | C++, QML | Its own renderer | Desktops, phones, embedded devices | | [Electron](https://www.electronjs.org), [Tauri](https://tauri.app) | Web pages; Rust for Tauri | A bundled Chromium; the system webview for Tauri | Desktops; phones for Tauri | ## Strengths - **Native views.** Text, input methods, selection, scrolling, accessibility and menus are the platform's own, so an app behaves as users expect there, where a framework that draws itself rebuilds each of them. What only one platform has, such as iOS's Liquid Glass, the others imitate the same way rather than drop. - **Fine-grained updates.** Components run once and signals update only what reads them, with no virtual DOM or per-frame view function to diff; animation runs on the platform's frame clock. - **The smallest packages and the fastest start.** In the [benchmarks](https://nsgo.dev/benchmarks/), the same counter app packs to about 8 MB on macOS, Windows and Linux, smaller than MyGo's, GPUI's and NSH's (33 to 45 MB for NSH); it also starts fastest, and takes less memory than frameworks that draw on the GPU, such as MyGo and GPUI. - **Go only.** The engine, layout and backends are Go, with no C, no cgo and no JavaScript engine for Go apps. - **Every platform from one Mac.** A Mac with Go and Xcode cross-compiles every target: no NDK, Android SDK, JDK, GTK or Windows toolchain. - **Every release file from one command.** `nsgo build` writes a signed, notarized disk image for macOS, a Windows installer, deb and Arch packages and an AppImage, packages for the Mac App Store, the Microsoft Store, Google Play and Flathub, and scripts that install from GitHub releases ([building](https://nsgo.dev/docs/building.md)). - **Each platform's own updates.** On macOS Sparkle, and on Windows WinSparkle, download, verify and install the next version; on Linux and Android the app finds it and opens its release page; on iOS the App Store does. An app a store installed is left to that store ([updates](https://nsgo.dev/docs/building.md#updates)). - **Every platform.** Phones, tablets, TVs, watches, desktops and the web share one engine and one API, along with iOS App Clips, iPhone and Mac widgets, and, experimentally, WeChat, QQ, Alipay and Douyin mini programs. - **Embedding.** An NSGo app can sit as a view in an existing iOS, Android, Go desktop or web app, to move over a screen at a time ([embedding](https://nsgo.dev/docs/embedding.md)). - **A fast dev loop.** Saving a Go file rebuilds and restarts the app; a TSX change updates the running app in place and keeps its state; build errors and panics while rendering show over the app ([running and debugging](https://nsgo.dev/docs/debugging.md)). - **One set of tests everywhere.** Tests find and use controls by text or role as users do, quickly with no window, or unchanged in real windows on each target with the platform's own input, saving screenshots ([testing](https://nsgo.dev/docs/testing.md)). - **Localization built in.** An app's and its components' words live in catalogs, and pseudo-locales find what is untranslated, cut off or not ready for right-to-left ([localization](https://nsgo.dev/docs/localization.md)). - **Shaders in Go.** `core.Shader` runs GPU shaders written in Go syntax or WGSL, on every platform ([canvas](https://nsgo.dev/docs/canvas.md#shaders)). - **TSX when wanted.** Screens can be written in TSX with Tailwind classes, on the same engine as Go components. ## Weaknesses - **Early.** No release and no stable API yet; the [roadmap](https://github.com/nsgo-dev/nsgo/blob/main/docs/roadmap.md) lists what is missing, such as more than one window on Windows and Linux, and a heap limit for TSX delivered at run time. - **A small ecosystem.** Few third-party components, examples or answers online, next to Flutter, React Native or Qt. - **Platforms differ.** Native views mean small differences between platforms, so an app needs checking on each. A framework that draws itself looks the same everywhere. - **Its own bridges.** On Android the Go program runs beside the Java runtime and talks to it over pipes, and on Linux users need GTK 4.14 or newer. - **TSX is interpreted.** TSX runs in goja, a JavaScript interpreter written in Go, slower than the engines React Native or Electron use. ## NSH NSGo ports NSH's design: the architecture, the backends, the API's names and `base`'s unstyled components are NSH's ([status](https://nsgo.dev/docs/index.md#status)). - **Choose NSGo** to write apps in Go rather than Rust or TSX, to build every target from a Mac with Go and Xcode, or for what NSGo adds: GPU shaders written in Go, vector icons, a terminal, apps opening their own windows, panels and tray icons, and styled components in `ui`. - **Choose NSH** for a more mature project, TSX run by QuickJS with bytecode and heap limits, the GPUI backend, or 32-bit Android. ## MyGo Both are UI written in Go without cgo, but MyGo draws its native UI itself on the GPU and rebuilds it from a view function every frame, where NSGo hands native views what changed. - **Choose MyGo** for desktop apps that look the same on every desktop, web-page windows with typed calls into Go, and its desktop APIs and shipping: tray icons, deep links, file associations, installers, notarization and signed delta updates. - **Choose NSGo** to reach phones too, to use each platform's own text, input and accessibility, or for state held in signals rather than redrawn from the app's data. ## Other Go toolkits - **Fyne and Gio** draw everything themselves, as MyGo does, and both reach phones and the web. Fyne gives a full widget set with its own look; Gio is immediate mode, low level and flexible. Both have released versions and users; neither looks or feels native. - **Wails** puts a web frontend in the system webview with Go behind it: any web library works, but the interface is a web page, desktop only. ## Frameworks in other languages - **Flutter and Compose Multiplatform** draw every pixel themselves: identical on every platform and mature, with large ecosystems, but with text, scrolling and input rebuilt rather than native, and Dart or Kotlin to learn. - **React Native** shares NSGo's model of a reactive core driving native views, and has the largest ecosystem of its kind. It centers on iOS and Android, and runs app logic in JavaScript; NSGo runs it in Go and treats desktops and the web as first-class. - **.NET MAUI** wraps each platform's controls in C#, without Linux or the web. **Avalonia** draws itself, close to WPF, on every desktop, phones and the web. - **Qt** is mature, fast and reaches embedded devices, but draws its own widgets, asks for C++ or QML, and is licensed under the LGPL or commercially. - **Electron and Tauri** show web pages: the web's tools and libraries in full, at the cost of a browser's memory, Electron's above all, and an interface that feels like a web page. ## Choosing Choose NSGo for an app that should feel native on phones, desktops and the web from one Go codebase, when an early project is acceptable. For an app that must ship soon on a stable API, take a mature framework from the table above: Flutter or React Native for phones, MyGo, Wails or Tauri for desktops alone. --- Source: https://nsgo.dev/docs/getting-started.md # Setup > Install what NSGo needs, create an app and run it on every target. ## Requirements - [Go](https://go.dev/dl/) 1.27.3 or later. - To build for iOS, tvOS and watchOS, a Mac with Xcode. Every other target builds from macOS with Go alone; the x86_64 Android emulator's also needs an Android NDK. - To run on an iPhone, an Apple ID in Xcode's Accounts and the iPhone trusted, with Developer Mode on. - To run on Android, `adb` from the Android SDK's platform-tools. - To run on Windows, Go alone, which also builds the Windows and web packages. - To run on Linux, GTK 4.14 or newer, Pango and Cairo; libsoup 3 for images from the network, WebKitGTK 6.0 for web views and GStreamer plug-ins for videos. ## Create an app Make a module, add NSGo and its tool, then let the tool write an app: ```sh mkdir hello && cd hello go mod init example.com/hello go get github.com/nsgo-dev/nsgo go get -tool github.com/nsgo-dev/nsgo/cmd/nsgo go tool nsgo init ``` `go get -tool` adds `nsgo` to the module once, so `go tool nsgo` runs it without installing anything. `init` writes: | File | What it is | | --- | --- | | `app.go` | The app, a counter: a `Root` component and `main`, which calls `nsgo.Run(Root)` | | `app_test.go` | A test that presses the button | | `nsgo.yaml` | The app's name, id, version and targets | `go tool nsgo init -template tsx` writes a TSX app instead, and `-template mixed` a Go app whose TSX entry uses Go components of its own. ## Run it ```sh go tool nsgo dev ``` `dev` builds the app for this machine and opens it. On each save it rebuilds and restarts the app, and a change of TSX alone updates the running app in place. A failed build shows over the app, with buttons to copy the error and to build again. Run it somewhere else with `-target`: ```sh go tool nsgo dev -target web # in the browser go tool nsgo dev -target ios_simulator # in the iOS simulator go tool nsgo dev -target tvos_simulator # in the Apple TV simulator go tool nsgo dev -target watchos_simulator # in the Apple Watch simulator go tool nsgo dev -target android # on a connected device or emulator ``` `go tool nsgo doctor` lists every target with whether this machine builds it and runs it, and what it lacks for those it cannot, such as Xcode, `adb`, a connected device or GTK. [Running and debugging](https://nsgo.dev/docs/debugging.md) covers devices, tracing updates, the performance monitor and other languages. ## With a coding agent NSGo's skill teaches coding agents, such as Claude Code, Codex and Cursor, how an NSGo app is written, tested and shipped. Add it to the app's directory, or with `-g` for every project: ```sh bunx skills add nsgo-dev/nsgo ``` Agents that read the docs themselves start from , which lists every page as Markdown at `https://nsgo.dev/docs/.md`; holds them all. An agent can also use the running app ([Running and debugging](https://nsgo.dev/docs/debugging.md#agents)). ## Next - [Example: A todo app](https://nsgo.dev/docs/tutorials/todo.md): state, a list and a test, in about ten minutes. - [Example: A notes app](https://nsgo.dev/docs/tutorials/notes.md): a navigation stack and notes kept on the device. - [Example: Popular GitHub repositories](https://nsgo.dev/docs/tutorials/repos.md): loading from the network with errors and a retry. --- Source: https://nsgo.dev/docs/tutorials/todo.md # Example: A todo app > Build a small todo app step by step, with signals, a list and a test. This tutorial builds a small todo app: add items, check them off and delete them. It takes about ten minutes and assumes the [setup](https://nsgo.dev/docs/getting-started.md). ## Create the app ```sh mkdir todo && cd todo go mod init example.com/todo go get github.com/nsgo-dev/nsgo go get -tool github.com/nsgo-dev/nsgo/cmd/nsgo go tool nsgo init go tool nsgo dev ``` ## Build the todo app The generated app is a counter. Replace the whole of `app.go` with this app: ```go package main import ( "fmt" "strings" "github.com/nsgo-dev/nsgo" "github.com/nsgo-dev/nsgo/core" "github.com/nsgo-dev/nsgo/icons/lucide" "github.com/nsgo-dev/nsgo/ui" ) type todo struct { id int text string done bool } func main() { nsgo.Run(Root) } func Root(cx *core.Scope) core.Node { items := core.NewSignalFunc(cx, []todo{}, nil) draft := core.NewSignal(cx, "") nextID := core.NewSignal(cx, 1) colors := ui.ColorsOf(cx) add := func() { text := strings.TrimSpace(draft.Get()) if text == "" { return } id := nextID.Get() nextID.Set(id + 1) items.Update(func(list []todo) []todo { return append(list, todo{id: id, text: text}) }) draft.Set("") } setDone := func(id int, done bool) { items.Update(func(list []todo) []todo { next := append([]todo(nil), list...) for i := range next { if next[i].id == id { next[i].done = done } } return next }) } remove := func(id int) { items.Update(func(list []todo) []todo { out := list[:0:0] for _, it := range list { if it.id != id { out = append(out, it) } } return out }) } left := func() int { n := 0 for _, it := range items.Get() { if !it.done { n++ } } return n } return core.View{ Style: core.Style{FlexGrow: 1, FlexShrink: 1, Padding: core.All(24), PaddingEnv: core.EnvAll(core.EnvSafeArea), Gap: 16}, StyleFunc: func() core.Style { return core.Style{Background: colors.Background()} }, Children: []core.Node{ ui.Text("Todos", 28, ui.Bold, colors.Text), core.View{Style: core.Style{Direction: core.Row, Gap: 8}, Children: []core.Node{ ui.Input{Value: draft.Get, OnChange: draft.Set, Placeholder: "What needs doing?", OnSubmit: func(string) { add() }, TestID: "draft", Style: core.Style{FlexGrow: 1}}, ui.Button{Text: "Add", Icon: lucide.Plus, OnPress: add, TestID: "add"}, }}, ui.Textf(func() string { return fmt.Sprintf("%d left", left()) }, 13, ui.Regular, colors.Muted), core.List{ Count: func() int { return len(items.Get()) }, Key: func(i int) any { return items.Get()[i].id }, EstimatedSize: 44, Style: core.Style{FlexGrow: 1}, Empty: ui.Text("Nothing yet. Add one above.", 13, ui.Regular, colors.Muted), Row: func(cx *core.Scope, row core.ListRow) core.Node { index := row.Index() it := items.Get()[index] return core.View{Style: core.Style{Direction: core.Row, AlignItems: core.AlignCenter, Gap: 8, Padding: core.Symmetric(8, 0)}, Children: []core.Node{ ui.Checkbox{ Checked: func() bool { for _, x := range items.Get() { if x.id == it.id { return x.done } } return false }, OnChange: func(done bool) { setDone(it.id, done) }, Text: it.text, TestID: fmt.Sprintf("todo-%d", it.id), Style: core.Style{FlexGrow: 1, FlexShrink: 1}, }, ui.IconButton{Icon: lucide.Trash, Label: "Delete", Variant: ui.IconButtonGhost, TestID: fmt.Sprintf("delete-%d", it.id), OnPress: func() { remove(it.id) }}, }} }, }, }, } } ``` Save the file and the app restarts. Type a task, press **Add** and check it off. ## How it works **State is signals.** `items`, `draft` and `nextID` are signals. A signal holds a value and tracks what reads it: when `items` changes, only the text and rows that read it update. `core.NewSignal` compares values with `==`, so a slice uses `core.NewSignalFunc` with no equality function. **Updates replace the slice.** `items.Update` returns a new slice, so the list's identity and the row keys decide what stays on screen. `nextID` gives each todo an ID, and `core.List`'s `Key` uses it; without a stable key, rows would reuse their cells for other items. **The screen is a tree of boxes.** `Root` returns a `core.View` with padding, a gap and the safe area's insets. `FlexGrow: 1` and `FlexShrink: 1` make it fill the window and no more, so a long list scrolls inside it instead of stretching it past the window's edge. The input row sets `Direction: core.Row`; everything else is the default column. **The input is controlled.** `draft.Get` is the field's value and `draft.Set` receives each change. `OnSubmit` adds a task when Return is pressed, and `add` clears the draft, so the field empties itself. **The list only builds what shows.** `core.List` asks for `Count` rows and builds a row only when it nears the screen. `Empty` shows while there are no items, and each row's checkbox and delete button carry their todo's ID, so a press still finds the right item after the list changes. **The look is `ui`'s.** `ui.Text`, `ui.Input`, `ui.Button`, `ui.Checkbox` and `ui.IconButton` take their colors from the theme and follow light and dark automatically. `ui.ColorsOf(cx)` gives the colors for the pieces the app draws itself, and a `StyleFunc` follows a change of theme in place. ## Test it The generated test presses the counter's button, so replace `app_test.go` with a test of the todo app: ```go package main import ( "testing" "time" "github.com/nsgo-dev/nsgo/nsgotest" ) func TestMain(m *testing.M) { nsgotest.Main(m) } func TestAddCompleteAndDelete(t *testing.T) { app := nsgotest.Start(t, Root) app.Find("text:Nothing yet. Add one above.") app.Type("id:draft", "Buy milk") app.Press("id:add") app.WaitFor("text:Buy milk", time.Second) app.Press("id:todo-1") app.WaitFor("text:0 left", time.Second) app.Press("id:delete-1") app.WaitFor("text:Nothing yet. Add one above.", time.Second) } ``` The test starts the app headless, finds elements by their text or test ID, and acts as a user would. Run it with: ```sh go tool nsgo test ``` `TestMain` gives the platform the main thread when the tests run on a target, and `go tool nsgo test -target ios_simulator` runs the same test in the simulator with its native input. ## Ship it ```sh go tool nsgo build ``` `build` writes a release package for each target `nsgo.yaml` lists, into `dist/--`: an `.app`, an `.ipa`, an `.apk`, Windows' and Linux's directories, or a web site. Everything to hand out also lands in `dist/releases`: the disk image, setup program, Debian and Arch packages, IPAs, APKs, the site, mini programs, and scripts that install the app from your GitHub releases. [Building and shipping](https://nsgo.dev/docs/building.md) covers installers, signing and notarization, publishing, icons, widgets, App Clips and the App Store. ## Next - [Example: A notes app](https://nsgo.dev/docs/tutorials/notes.md) and [Example: Popular GitHub repositories](https://nsgo.dev/docs/tutorials/repos.md): two more apps, step by step. - [Components and state](https://nsgo.dev/docs/components.md): elements, components, signals and regions in more depth. - [Layout](https://nsgo.dev/docs/layout.md) and [Styles and themes](https://nsgo.dev/docs/styles.md): the screen's structure and look. - [Lists and scrolling](https://nsgo.dev/docs/lists.md): long lists, sticky headers and loading more. - [Testing](https://nsgo.dev/docs/testing.md): find controls, press them and take screenshots. - [Embedding](https://nsgo.dev/docs/embedding.md): put the app inside another app. --- Source: https://nsgo.dev/docs/tutorials/notes.md # Example: A notes app > Build a notes app with a navigation stack, a multiline editor and notes kept on the device. This tutorial builds a notes app: a list of notes, a page that edits one, and notes that are still there when the app starts again. It assumes the [setup](https://nsgo.dev/docs/getting-started.md). ## Create the app ```sh mkdir notes && cd notes go mod init example.com/notes go get github.com/nsgo-dev/nsgo go get -tool github.com/nsgo-dev/nsgo/cmd/nsgo go tool nsgo init go tool nsgo dev ``` ## Write the app Replace the whole of `app.go`: ```go package main import ( "encoding/json" "strings" "github.com/nsgo-dev/nsgo" "github.com/nsgo-dev/nsgo/base" "github.com/nsgo-dev/nsgo/core" "github.com/nsgo-dev/nsgo/icons/lucide" "github.com/nsgo-dev/nsgo/ui" ) type note struct { ID int `json:"id"` Text string `json:"text"` } // title is a note's first line. func (n note) title() string { line, _, _ := strings.Cut(strings.TrimSpace(n.Text), "\n") if line == "" { return "New note" } return line } func main() { nsgo.Run(Root) } func Root(cx *core.Scope) core.Node { store := cx.Storage() var saved []note if data, ok := store.GetItem("notes"); ok { json.Unmarshal([]byte(data), &saved) } notes := core.NewSignalFunc(cx, saved, nil) save := func(list []note) { notes.Set(list) data, _ := json.Marshal(list) store.SetItem("notes", string(data)) } // A route is the ID of the note it edits, and 0 the list. nav := base.NewNav(cx, 0) find := func(id int) note { for _, n := range notes.Get() { if n.ID == id { return n } } return note{ID: id} } add := func() { id := 1 for _, n := range notes.Get() { id = max(id, n.ID+1) } save(append([]note{{ID: id}}, notes.Get()...)) nav.Push(id) } edit := func(id int, text string) { list := append([]note(nil), notes.Get()...) for i := range list { if list[i].ID == id { list[i].Text = text } } save(list) } remove := func(id int) { var list []note for _, n := range notes.Get() { if n.ID != id { list = append(list, n) } } save(list) nav.Pop(1) } return nav.Stack(core.Style{FlexGrow: 1}, func(cx *core.Scope, id int, _ func() int) base.NavPage { if id == 0 { return listPage(cx, notes.Get, nav.Push, add) } return ui.NavPage(cx, base.NavPage{ TitleFunc: func() string { return find(id).title() }, Children: []core.Node{core.View{Style: core.Style{FlexGrow: 1, FlexShrink: 1, Padding: core.All(16)}, Children: []core.Node{ ui.Input{ Multiline: true, Value: func() string { return find(id).Text }, OnChange: func(text string) { edit(id, text) }, Placeholder: "Write something", TestID: "editor", Style: core.Style{FlexGrow: 1, FlexShrink: 1}, }, }}}, }, ui.NavHeader{ OnBack: func() { nav.Pop(1) }, BackLabel: "Notes", Trailing: []core.Node{ui.IconButton{Icon: lucide.Trash, Label: "Delete", Variant: ui.IconButtonGhost, TestID: "delete", OnPress: func() { remove(id) }}}, }) }) } func listPage(cx *core.Scope, notes func() []note, open func(int), add func()) base.NavPage { colors := ui.ColorsOf(cx) return ui.NavPage(cx, base.NavPage{ Title: "Notes", Children: []core.Node{core.List{ Count: func() int { return len(notes()) }, Key: func(i int) any { return notes()[i].ID }, EstimatedSize: 48, Style: core.Style{FlexGrow: 1}, Empty: core.View{Style: core.Style{Padding: core.All(16)}, Children: []core.Node{ ui.Text("No notes yet.", 13, ui.Regular, colors.Muted), }}, Row: func(cx *core.Scope, row core.ListRow) core.Node { n := notes()[row.Index()] return core.View{ Role: core.RoleButton, OnPress: func(core.PressEvent) { open(n.ID) }, Style: core.Style{Padding: core.Symmetric(12, 16), Borders: core.Edges{Bottom: 1}}, StyleFunc: func() core.Style { return core.Style{BorderColor: colors.Border()} }, Children: []core.Node{ui.Text(n.title(), 15, ui.Regular, colors.Text)}, } }, }}, }, ui.NavHeader{ Trailing: []core.Node{ui.IconButton{Icon: lucide.Plus, Label: "New note", Variant: ui.IconButtonGhost, TestID: "new", OnPress: add}}, }) } ``` Press **+** to add a note and type; its first line becomes its title in the list. Quit the app and start it again: the notes are still there. ## How it works **A stack of pages.** `base.NewNav(cx, 0)` keeps a stack of routes, here note IDs with `0` for the list. `nav.Stack` builds a page for each route and keeps the pages below mounted, so the list keeps its scroll while a note is open. `Push` opens a note and `Pop` goes back. **Pages with a header.** `ui.NavPage` gives a page `ui`'s background and a `ui.NavHeader` with its title, back button and buttons. On iOS and Android the system draws the header, with its own back gesture and transition; elsewhere `ui` draws it. **Storage on the device.** `cx.Storage()` is the app's key-value storage. `Root` reads the notes once as it starts, and `save` writes them as JSON whenever they change. Storage holds strings; files the app writes go in `cx.DataDir()`. **One source of truth.** Every page reads the same `notes` signal. The editor's `Value` reads the note, `OnChange` saves it, and the header's `TitleFunc` and the list's row follow the same signal, so they update as you type. ## Test it Replace `app_test.go`. `nsgotest.Storage` starts the app with storage already written, as if it had run before: ```go package main import ( "strings" "testing" "time" "github.com/nsgo-dev/nsgo/nsgotest" ) func TestMain(m *testing.M) { nsgotest.Main(m) } func TestWriteANote(t *testing.T) { app := nsgotest.Start(t, Root) app.Find("text:No notes yet.") app.Press("id:new") app.Type("id:editor", "Groceries\nMilk") app.WaitFor("role:header Groceries", time.Second) if data, _ := app.Storage().GetItem("notes"); !strings.Contains(data, "Milk") { t.Fatalf("saved %q", data) } } func TestSavedNotes(t *testing.T) { app := nsgotest.Start(t, Root, nsgotest.Storage(map[string]string{ "notes": `[{"id":1,"text":"Ideas\nA notes app"}]`, })) app.Press("text:Ideas") app.Press("id:delete") app.WaitFor("text:No notes yet.", time.Second) } ``` ```sh go tool nsgo test ``` ## Next - [Example: Popular GitHub repositories](https://nsgo.dev/docs/tutorials/repos.md): fetch from the network without blocking the screen. - [Navigation](https://nsgo.dev/docs/navigation.md): modals, menus and tab bars. - [Platform services](https://nsgo.dev/docs/services.md): files, sharing, the clipboard and more. --- Source: https://nsgo.dev/docs/tutorials/repos.md # Example: Popular GitHub repositories > List the most-starred Go repositories from GitHub's API, loading on a goroutine with a spinner, errors and a retry. This tutorial builds an app that lists the most-starred Go repositories on GitHub. It covers what every app that talks to a server needs: a request that does not block the screen, a loading indicator, an error with a retry, and a test that runs without the network. It assumes the [setup](https://nsgo.dev/docs/getting-started.md). ## Create the app ```sh mkdir repos && cd repos go mod init example.com/repos go get github.com/nsgo-dev/nsgo go get -tool github.com/nsgo-dev/nsgo/cmd/nsgo go tool nsgo init go tool nsgo dev ``` ## Write the app Replace the whole of `app.go`: ```go package main import ( "context" "encoding/json" "fmt" "net/http" "github.com/nsgo-dev/nsgo" "github.com/nsgo-dev/nsgo/core" "github.com/nsgo-dev/nsgo/ui" ) // api lists the most-starred Go repositories on GitHub. var api = "https://api.github.com/search/repositories?q=language:go&sort=stars&per_page=30" type repo struct { Name string `json:"full_name"` Description string `json:"description"` Stars int `json:"stargazers_count"` } // fetch runs on its own goroutine, never the engine's. ctx is cancelled // once its answer no longer counts. func fetch(ctx context.Context) ([]repo, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, api, nil) if err != nil { return nil, err } resp, err := http.DefaultClient.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("GitHub answered %s", resp.Status) } var body struct { Items []repo `json:"items"` } err = json.NewDecoder(resp.Body).Decode(&body) return body.Items, err } func main() { nsgo.Run(Root) } func Root(cx *core.Scope) core.Node { repos := core.NewResource(cx, nil, func(ctx context.Context, _ struct{}, _ core.ResourceInfo) ([]repo, error) { return fetch(ctx) }) list := func() []repo { list, _ := repos.Get() return list } failure := func() string { if err := repos.Err(); err != nil { return err.Error() } return "" } colors := ui.ColorsOf(cx) return core.View{ Style: core.Style{FlexGrow: 1, FlexShrink: 1, PaddingEnv: core.EnvAll(core.EnvSafeArea)}, StyleFunc: func() core.Style { return core.Style{Background: colors.Background()} }, Children: []core.Node{ core.View{Style: core.Style{Direction: core.Row, AlignItems: core.AlignCenter, Gap: 12, Padding: core.All(16)}, Children: []core.Node{ ui.Text("Popular Go repositories", 22, ui.Bold, colors.Text), core.Show{When: repos.Loading, Then: func(*core.Scope) core.Node { return ui.Spinner{Label: "Loading"} }}, }}, core.Show{ When: func() bool { return repos.State() == core.ResourceErrored }, Then: func(*core.Scope) core.Node { return core.View{Style: core.Style{Padding: core.All(16), Gap: 12, AlignItems: core.AlignStart}, Children: []core.Node{ ui.Textf(failure, 14, ui.Regular, colors.Muted), ui.Button{Text: "Try again", OnPress: repos.Refetch}, }} }, Else: func(*core.Scope) core.Node { return core.List{ Count: func() int { return len(list()) }, Key: func(i int) any { return list()[i].Name }, EstimatedSize: 64, Style: core.Style{FlexGrow: 1}, Row: func(cx *core.Scope, row core.ListRow) core.Node { r := list()[row.Index()] return core.View{Style: core.Style{Padding: core.Symmetric(10, 16), Gap: 2}, Children: []core.Node{ core.View{Style: core.Style{Direction: core.Row, JustifyContent: core.JustifySpaceBetween, Gap: 8}, Children: []core.Node{ ui.Text(r.Name, 15, ui.Medium, colors.Text), ui.Text(fmt.Sprintf("★ %d", r.Stars), 13, ui.Regular, colors.Muted), }}, ui.Text(r.Description, 13, ui.Regular, colors.Muted), }} }, } }, }, }, } } ``` The spinner shows while the request runs, then the list fills in. ## How it works **Go's own networking.** `net/http` works on every target, with the system's TLS and proxy settings, so `fetch` is plain Go. **Off the engine's goroutine.** Components and signals belong to the engine's goroutine, which must never wait on the network. `core.NewResource` runs `fetch` on a goroutine of its own and brings the result back to the engine. Its source is nil, so it loads once, and **Try again** calls `Refetch` to load again. Only the latest load counts: leaving the screen cancels the request through `ctx`, and a late answer never replaces a newer one. **State as signals.** `repos.Get`, `repos.Err`, `repos.State` and `repos.Loading` are signals, so bindings that read them follow each load. `core.Show` shows the spinner while it loads, and either the error with **Try again** or the list, rebuilding only that part when its condition changes. A retry keeps the last list until the new one arrives. **A list of any length.** `core.List` builds only the rows near the screen, keyed by the repository's name, so a new result reuses the rows of the same repositories. ## Test it The request's URL is a variable, so a test points it at a local server with `httptest` and controls what the "network" answers: ```go package main import ( "net/http" "net/http/httptest" "testing" "time" "github.com/nsgo-dev/nsgo/nsgotest" ) func TestMain(m *testing.M) { nsgotest.Main(m) } // serve answers the app's requests with body instead of GitHub. func serve(t *testing.T, status int, body string) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(status) w.Write([]byte(body)) })) t.Cleanup(server.Close) api = server.URL } func TestShowsRepositories(t *testing.T) { serve(t, http.StatusOK, `{"items":[{"full_name":"golang/go","description":"The Go programming language","stargazers_count":130000}]}`) app := nsgotest.Start(t, Root) app.WaitFor("text:golang/go", 5*time.Second) app.Find("text:★ 130000") } func TestRetriesAfterAFailure(t *testing.T) { serve(t, http.StatusServiceUnavailable, "") app := nsgotest.Start(t, Root) app.WaitFor("text:Try again", 5*time.Second) } ``` ```sh go tool nsgo test ``` `WaitFor` waits until the answer has arrived and the screen shows it. ## Next - [Platform services](https://nsgo.dev/docs/services.md#networking): more on networking, storage and files. - [Lists and scrolling](https://nsgo.dev/docs/lists.md): pull to refresh, snapping and long lists. - [Running and debugging](https://nsgo.dev/docs/debugging.md): watch each update with `-trace`. --- Source: https://nsgo.dev/docs/debugging.md # Running and debugging > Run an app on any target with hot reload, and see its errors, updates, layout, speed and other languages. ## Run ```sh go tool nsgo dev ``` `dev` builds the app for this machine and opens it, or for the target `nsgo.yaml`'s `dev.target` names. On each save of a Go file it rebuilds and restarts the app, running `go mod tidy` first when the app imports a package whose module `go.mod` or `go.sum` lacks; a change of TSX alone updates the running app in place, keeping its state. The run ends with the app. `-target` runs it elsewhere, and `-device` picks the iPhone, Apple TV, Apple Watch, simulator or Android device by name, UDID or serial: ```sh go tool nsgo dev -target web # in the browser, at the address it prints, on -port (8080 by default) go tool nsgo dev -target ios_simulator # in the iOS simulator go tool nsgo dev -target ios # on the connected iPhone, signed by Xcode go tool nsgo dev -target tvos_simulator # in the Apple TV simulator go tool nsgo dev -target watchos_simulator # in the Apple Watch simulator go tool nsgo dev -target android # on a connected device or emulator, over adb ``` `-h` lists every flag. ## Errors A failed build shows over the running app, with buttons to copy the error and to build again; fix the file and the next save replaces it. A component that panics while it renders shows the same way. The app's own output, such as `log.Print`, appears in the terminal under the tool's lines. Before running, `go tool nsgo check` vets the app and type-checks its TSX; `-target all` vets it for every target, so code behind another platform's build tags is checked from a Mac too. ## DevTools ```sh go tool nsgo dev -devtools ``` `-devtools` opens DevTools in the browser, served by `nsgo dev` for every target, a phone's app too. It shows the running app as it runs: - **Elements**: the views in a tree. Each shows the component that built it, a link to its line in your editor, the selector a test finds it by, its classes, its bounds, its margin, border and padding as laid out, and the style it sets. Hovering a row draws its margin, padding and content in the app, as a browser's DevTools do, and **Pick** lets you press a view in the app to find it in the tree. - **State**: the signals your code made, under the components and computations that own them, with their values, memos' values, and how many computations read each. A value no computation reads changes nothing on screen. Strings, booleans and numbers can be edited, so you can try a state without a code change. Values NSGo made for itself are behind **NSGo's own**. - **Profiler**: each commit's time, split into your code before it, the commit with its layout, and applying it on the platform's main thread. It also shows the lines whose writes caused the commit, and opens on the computations it ran, the longest first. A second list names the computations that ran most in the last second, by the line that made them. Android's runtime applies batches in its own Java code, so Android commits have no platform time. - **Recorder**: what you press, type, clear and scroll in the app, written as an `nsgotest` test file to copy into a `_test.go` file. In Elements, **Assert shown** and **Assert text** add a check of the selected view. A text check holds the text in the language the app showed it in, so record with the language your tests run in. - **Highlight updates** outlines each view a commit changes, as `-highlight` does. - **Layout** shows and hides the [layout audit](#layout) on the device, as `-layout` starts it. A TSX app's state, computations and writes are named by their TSX lines, through the bundles' source maps, and link to the files in your app. DevTools read the app through `nsgo dev`'s server, which listens on your network so phones reach it. Each run makes a token, which the addresses `nsgo dev` prints and opens for DevTools and the web target carry after `#`, and the server answers only requests that hold it, so open DevTools and the web target from those addresses. Anyone you give one to can change the app's state while it runs and add steps to the test the Recorder writes, so read a recorded test before you run it. Others on your network and web pages of other sites you visit reach none of it. ## Updates ```sh go tool nsgo dev -highlight ``` `-highlight` outlines for half a second each view a commit changed, so an update that redraws more than it should shows on screen as you use the app. The outlines draw over the app and its modals. DevTools turn them on and off while the app runs. ```sh go tool nsgo dev -trace ``` `-trace` prints each commit of the running app: its number, its time, and the mount items it sent, so you see which views an update created, changed or removed. Above the items it names what the commit followed from: the lines of your code that wrote signals since the last commit, the most writing first, and how many bindings, effects and memos ran. ```text ── commit 12: 40 items, commit 1.65 ms after app/market.go:473 ×4, app/market.go:487 ×4, app/market.go:384 ran 118 bindings, 18 effects, 5 memos ``` A line that writes many times, or many bindings run for one write, is where to batch writes (`cx.Batch`) or narrow what a binding reads. `go tool nsgo test -trace` prints the same under each step of a test. ## Layout ```sh go tool nsgo dev -layout go tool nsgo dev -layout=bounds,grid=4 ``` `-layout` starts the app with DevTools' layout audit over it, which outlines every view: its border box in blue, the inner edge of its padding in green and the outer edge of its margin in orange, each cut where a scroll view or a view that clips hides it. `grid` draws a grid of 8 points, and `grid=N` one of N points, at least 4. Give the value after `=`: the flag alone means `bounds`. A readout in the bottom corner shows the window's size in points, with buttons that turn the boxes, the grid and the ruler on and off. While the ruler is on, presses pick two points instead of reaching the app, and the readout shows how far apart they are across and down; a third press starts again. | Key | Effect | | --- | --- | | `Cmd+Option+Shift+L`, `Ctrl+Alt+Shift+L` | Shows or hides the audit, in a build with `-layout` | | `Esc`, back | Stops measuring | If your app binds the same key, the app's binding wins. Without the key, as on a phone, run with `-devtools` too and show and hide the audit with DevTools' **Layout**; hide it to reach a control under the readout. The boxes are where layout placed the views, before transforms and animations move them, and follow a scroll a frame behind it. Redrawing them costs every update time, so measure speed without them. ## Speed ```sh go tool nsgo dev -fps go tool nsgo dev -release -fps ``` `-fps` shows the performance monitor over the app on macOS, iOS, Windows and Linux: what its frames and commits cost while it runs. Dev builds carry the dev client; measure with `-release`, which builds as `nsgo build` does. ## Languages and appearance ```sh go tool nsgo dev -lang zh-Hans go tool nsgo dev -lang en-XA # accented and longer, to find untranslated and cut text go tool nsgo dev -lang ar-XB # right to left ``` `-lang` starts the app in a locale, a pseudo locale too ([Localization](https://nsgo.dev/docs/localization.md)). ## Agents ```sh go tool nsgo dev -automation ``` `-automation` lets an AI agent or an end-to-end runner use the running app as a person does, on macOS so far: list its windows, outline what one shows, press, type, send keys, scroll, drag, hover, choose menu items, answer alerts, wait for what appears and take screenshots. Agents reach it over MCP through `go tool nsgo mcp`, which uses the `dev -automation` running for the directory, or starts one with the same flags and stops it as the client leaves: ```sh claude mcp add nsgo -- go tool nsgo mcp ``` Other clients take the same command, `go` with the arguments `tool nsgo mcp`, run in the app's directory. Views are named with the selectors of [tests](https://nsgo.dev/docs/testing.md#a-test), or with the `view:` references a snapshot prints, which a save makes stale as it restarts the app. The agent acts on the app's real data, as `dev` runs it. Only `dev -automation` builds carry it, and only tools on this machine holding the token in `.nsgo/automation.yaml`, which only you can read, reach it. ## Flags of every build Every build of an app, packages included, takes NSGo's own flags as it starts, which override `nsgo.yaml` and the app's code and never reach the app's own flags: | Flag | Effect | | --- | --- | | `--nsgo-fps` | Shows the performance monitor on macOS, iOS, Windows and Linux | | `--nsgo-lang=zh-Hans` | Starts the app in a locale, a pseudo locale too | | `--nsgo-appearance=dark` | Starts the app `light`, `dark` or following the `system` | ```sh ./Hello --nsgo-fps # Windows and Linux open Hello.app --args --nsgo-fps # macOS xcrun simctl launch booted dev.example.hello --nsgo-fps adb shell am start -n dev.example.hello/dev.oneo.nsgo.NsgoActivity --ez nsgo-fps true --es nsgo-lang de ``` On the web they go in the page's query: `?nsgo-fps&nsgo-lang=de`. --- Source: https://nsgo.dev/docs/testing.md # Testing > Test an app the way a user uses it, headless or in a native window. ## A test `nsgotest` starts the app without a window. A test finds controls by their text or role and acts on them as a user would: ```go 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") } ``` `TestMain` gives the platform the main thread when the tests run on a target. ## Run the tests ```sh go tool nsgo test ``` `test` prints a line per test. `-run` picks tests as `go test`'s does, `-junit report.xml` writes a JUnit report, `-trace` prints each step's mount items and commit times and `-bench` runs benchmarks instead, a line per result. Flags after `--` go to `go test`. Given a directory without `nsgo.yaml`, such as a library or the module's root, `test` takes every package under it; a directory may also end in `/...`, as Go's patterns do. `go tool nsgo check` takes packages the same way and vets them. ## Run on a platform The same tests run in a real window with the platform's own input: ```sh go tool nsgo test -target macos ``` The targets are `macos`, `windows`, `linux`, `web` (in Chrome, Chromium or Edge), `ios_simulator`, `tvos_simulator` (the remote's arrows, Return as its select button and Escape as Menu), `watchos_simulator` and `android`, or `all`. Each test runs in a process of its own. `app.Capture()` returns the current pixels as an `image.Image` and `app.CapturePNG()` the PNG bytes, both skipping the test headless. Their pixels are in the display's colour space, Display P3 on a Mac with a P3 display: the PNG names it in its colour profile, which `image/png` ignores, so a colour read from `app.Capture()` is that space's and not sRGB's, and NSGo does not convert it. `app.Screenshot("en/library")` saves a screenshot as `.nsgo/test/screenshots//en/library.png`, `.nsgo/test/screenshots/macos/en/library.png` on `macos`; names may contain spaces but cannot be absolute or contain `..`. `-screenshots dir` saves them under `dir` instead, a path relative to the working directory, and a screenshot whose path an earlier one took fails its test. `-fps` logs the performance monitor's readings each second on `macos`, `windows`, `linux` and `ios_simulator`, and the engine's alone on `web` and `android`. `-env KEY=VALUE` sets a variable of the tests' environment, which a phone's tests see no other way. To measure rather than check, start with `nsgotest.Measure()`: the app runs on the system's clock, `Advance` waits as long as it moves, and steps wait only for what animates to rest. `app.Mark("name")` names what the readings from there on measure, and `app.Mark("-")` ends it; the marks print with or without `-fps`. ## TSX tests A TSX app's `*.test.tsx` files, written with `nsgo/test`, run with its Go tests. To script a TSX app without a test file, pass its entry and the steps on standard input, written as a `*.test.tsx` test's body with `app` the app: ```sh go tool nsgo test App.tsx < steps.ts ``` --- Source: https://nsgo.dev/docs/building.md # Building and shipping > Package an app for every platform, make installers, sign and notarize it, publish it on GitHub and embed it in other apps. ## Build ```sh go tool nsgo build ``` `build` writes a stripped release package for each target `nsgo.yaml` lists, into `dist/--`: an `.app`, an `.ipa`, an `.apk`, Windows' and Linux's directories, or a web site, whose page fetches each TSX module an `import()` names when the import runs. `-target` replaces the list, and `all` means every target that needs only Go and Xcode; add `android_x64` when an Android NDK is installed, and `miniprogram`, or `miniprogram_wechat`, `miniprogram_qq`, `miniprogram_alipay` or `miniprogram_douyin` alone, for [mini programs](#mini-programs). `-run` starts the package built for this machine, or installs it on the iPhone, simulator or Android device `-device` picks. Everything to hand out lands in `dist/releases`, the only directory to look in: | In `dist/releases` | What | | --- | --- | | `*.dmg`, `*-setup.exe`, `*.deb`, `*.pkg.tar.gz`, `*.AppImage` | [Installers](#installers) for macOS, Windows and Linux | | `*-ios.ipa`, `*-tvos.ipa`, `*.apk` | The iOS, tvOS and Android packages, for the stores or devices | | `*.pkg`, `*.msix`, `*.aab`, `*.tar.gz` and `flatpak/` | A `store` build's [store packages](#store-packages) | | `install.sh`, `install.ps1` | Scripts installing the app from [GitHub releases](#publish-on-github) | | `appcast-.xml` | The feeds [updating](#updates) apps read | | `*-web.zip` | The site, to unpack and serve as it is | | `miniprogram//` | Each mini program's project, to upload from its developer tool | Each file is named `---`, such as `hello-1.2.0-windows-x64-setup.exe`, so users find their system's on a release page, and has its `.sha256` beside it. A build of another version empties the directory first; builds of the same version, on other machines too, add to it. Simulators' and watchOS's packages stay in their `dist/--` directories. ## nsgo.yaml ```yaml name: Hello id: dev.example.hello version: 0.1.0 build_number: 1 # CFBundleVersion and Android's versionCode; else from version # appearance: dark # light or dark whatever the system's # theme: theme.json # a TSX app's theme: NSGo's or a shadcn/ui one # i18n: { dir: locales, source: en } # the app's catalogs and the language they are written in icon: apple: assets/AppIcon.icon # macOS, iOS and watchOS, or an .icns for macOS png: assets/Icon.png # every other target tvos: assets/AppIconTV.brandassets # the Apple TV's layered icon and top shelf art, or a wide PNG watchos: assets/AppIconWatch.icon # the Apple Watch's own, drawn in circles, or a square PNG dev: target: native # native, web, ios, ios_simulator, tvos, tvos_simulator, watchos, watchos_simulator, android or miniprogram_ hot_reload: true # false builds once, watching no files # icon: { png: assets/Icon-dev.png } # replaces icon's files in dev's builds; defaults to assets/*-dev.* build: output: dist open: false # true opens the output directory after building installers: true # false leaves out the disk image, the Windows installer and the Linux packages jobs: 0 # how many targets build at once; 0 is half the CPUs, 1 one after another targets: [macos_arm64, macos_x64, windows_arm64, windows_x64, linux_arm64, linux_x64, ios, ios_simulator, tvos, tvos_simulator, watchos, watchos_simulator, android_arm64, web] # android: { permissions: [REQUEST_INSTALL_PACKAGES] } # manifest permissions beyond the runtime's # windows: { command_line: true } # add .com, a console companion that forwards arguments and streams to .exe; not in store builds # linux: { maintainer: "Jane Doe " } # the Linux packages' maintainer, else the app's name # release: { github: me/hello } # the GitHub repository the install scripts and updates install from, else the Git remote origin's; tag_prefix: v by default # updates: { key: } # how the app updates, by platform and channel: see Updates # miniprogram: { appid: { wechat: wx0123456789abcdef, alipay: "2021000000000000" }, custom_navigation: false } # the mini programs' ``` Flags replace the values they name, such as `-target`, `-version` or `-output`; `-h` lists them all. An Apple TV's icon is a layered card, not a square: `tvos` takes Xcode's `.brandassets`, its `App Icon` stacks and top shelf images, or a wide image such as 1280×768, which the tool draws to fit the card over its edge color and centers on the top shelf images. Without either, the PNG sits at three quarters of the card's height. An Apple Watch masks its icon to a circle, so `watchos` takes an `.icon` drawn for watchOS's circles or a square PNG; without it the app uses `apple`, then `png`. Both fall back to `assets/AppIconTV.*` and `assets/AppIconWatch.*`, as `apple` and `png` do. Every build also takes NSGo's own flags as it starts, such as `--nsgo-fps` ([Running and debugging](https://nsgo.dev/docs/debugging.md#flags-of-every-build)). ## Installers For the desktop targets, `build` writes what users install the app from into `dist/releases`, each with a `.sha256` checksum: | Target | Installer | | --- | --- | | macOS | `--macos-.dmg`, a disk image showing the app, an arrow and a link to Applications to drag it to, signed with `sign.identity` and [notarized](#notarize) | | Windows | `--windows--setup.exe`, an [NSIS](https://nsis.sourceforge.io) installer that installs for the user, again in the folder the user chose before, closing the app if it runs, with a Start menu shortcut, an optional desktop one and an uninstaller that removes only what it installed; `windows.command_line` also installs the `.com` companion and puts the app's folder on the user's `Path`, so consoles opened after installing run the app's command, while Win+R, which finds the companion too, starts the app and closes the console it opened | | Linux | `--linux-.deb` for Debian and Ubuntu, and `--linux-.pkg.tar.gz` for Arch Linux, which install the app in `/opt/` with a command, its desktop entry and icon, and `--linux-.AppImage`, which runs on any distribution without installing | The Windows installer needs NSIS's compiler, on a Mac `brew install makensis`, so a Mac builds every installer; without it `build` leaves the script, `.nsi`, in the package, which compiles anywhere as it is. The installer speaks the languages of the app's catalogs that NSIS translates, Simplified and Traditional Chinese included, picking the system's. `-installers=false` or `build.installers: false` builds the packages alone. The installers take pictures and icons of the app's own; each is the app's icon or the built-in picture otherwise: ```yaml macos: dmg_background: assets/DMGBackground.png # the disk image's window, which takes its size in points dmg_icon: assets/DMGIcon.png # the disk image's icon: an .icns or a square PNG windows: installer_icon: assets/Setup.png # the setup program's icon: an .ico or a square PNG uninstaller_icon: assets/Uninstall.png # the uninstaller's ``` A background PNG with a twin twice its size, such as `DMGBackground@2x.png`, stays sharp on Retina screens. The app's icon sits a quarter in from the left, Applications a quarter in from the right, each with its name centered from top to bottom, where the built-in picture draws its arrow; leave room for them in a background of your own. The Linux packages have no pictures of their own; they install the app's icon. The AppImage needs FUSE, which most distributions have, and takes GTK 4 from the system, as the packages do; its runtime is downloaded once into the user cache. ## Store packages `-channel store`, or `updates.channel: store`, builds the desktop and Android targets for their stores, which update them, in place of the installers and install scripts: | Target | Store package | | --- | --- | | macOS | `--macos-.pkg` for the Mac App Store: the app in the App Sandbox, signed with the provisioning profile's identity and packaged with your team's installer identity | | Windows | `--windows-.msix` for the Microsoft Store, which signs it on publishing | | Linux | `--linux-.tar.gz` with the app's AppStream metadata, and `flatpak/.yml`, the manifest Flathub builds the Flatpak from, which downloads those archives from the release on GNOME's runtime | | Android | `--android-.aab`, an App Bundle for Google Play signed with `sign.keystore`, the upload key | ```yaml macos: category: public.app-category.productivity # LSApplicationCategoryType, which the Mac App Store requires # entitlements: { com.apple.security.files.downloads.read-write: true } # beyond the network, the files the user picks and what apple.info explains windows: store: # as Partner Center's Product identity lists them identity: 12345Example.Hello publisher: CN=00000000-0000-0000-0000-000000000000 publisher_name: Example # device_capabilities: [microphone, webcam] # the devices the app uses, which Windows asks the user for linux: summary: Says hello # one line for software centers license: MIT # an SPDX expression; LicenseRef-proprietary by default # flatpak_runtime: "51" # GNOME's runtime version android: target_sdk: 35 # the targetSdkVersion Google Play requires sign: macos_profile: assets/MacAppStore.provisionprofile # the Mac App Store's profile # installer: "3rd Party Mac Developer Installer: Example (ABCDE12345)" # by default the keychain's of the profile's team ``` Without `sign.macos_profile` the Mac package is unsigned: it runs sandboxed on your Mac, which tests the sandbox, but uploads nowhere. A sandboxed app reaches only its own container, the network and the files the user picks; widgets cannot share timelines from the sandbox yet, so a store build refuses them. Upload the `.pkg` with Transporter, the `.msix` in Partner Center, the `.aab` in Play Console, and submit `flatpak/.yml` to Flathub once the release holds the archives it names; `flatpak-builder` builds it locally too. Windows installs an MSIX outside the Store only when it is signed, which the Store does. On macOS, a scroll view's edge effects (`Scroll.EdgeEffects`) blur through Core Animation's private `CABackdropLayer` and `CAFilter`, as AppKit's own edge effects do, since no public API varies a blur across a view. App Store review may reject apps that use private APIs: NSGo calls them only for scroll views with edge bars, but every macOS build contains their names. ## Publish on GitHub When the build has a macOS, Windows or Linux target, `dist/releases` also holds `install.sh` and `install.ps1`, which install the app from the releases of the GitHub repository `release.github` names, else the one the app's Git remote `origin` is on; without either `build` writes none and says so. Upload its files, the top level without `miniprogram/`, to the release tagged `v`: ```sh gh release create v1.2.0 dist/releases/*.* ``` Users then install or update the app with one line, which the build prints: ```sh curl -fsSL https://github.com/me/hello/releases/latest/download/install.sh | sh # macOS and Linux ``` ```powershell irm https://github.com/me/hello/releases/latest/download/install.ps1 | iex # Windows ``` On macOS the script copies the app from the disk image into `/Applications`, or `~/Applications` when it cannot write there; on Linux it installs the Arch package with `pacman`, else the Debian one with `apt-get` or `dpkg`, through `sudo`; on Windows it runs the installer silently, which installs where the app already is. Each checks the file against its checksum first. `_VERSION`, the app's name in capitals, such as `HELLO_VERSION=1.1.0`, installs another release, and `_DOWNLOAD_URL` downloads from a mirror. ## Updates `updates` has the app keep itself up to date the way its platform and the channel users get it from do: ```yaml updates: key: zE0hx2FZwwHy6X7LbEP3GjTTE3KWfCAp5gsaBysC+vg= # the public key nsgo keygen prints # channel: store # direct, store or none; direct by default with a key, none without # feed: https://example.com/hello/ # where the appcasts and installers are; by default the latest GitHub release # automatic: false # check only when asked; true by default ``` `nsgo keygen` prints a key pair and writes no file: put the public key in `updates.key` and commit it, and keep the secret one out of the repository, in a password manager and in `NSGO_UPDATES_KEY` where releases build, such as a GitHub Actions secret. Each direct build signs the installers with the secret key and writes `appcast-.xml` beside them, a feed in [Sparkle](https://sparkle-project.org)'s format that the app reads under `feed`: upload the appcasts with the installers. Without the secret key, `build` writes no macOS or Windows appcast, as those install only signed installers, and Linux and Android apps reading an unsigned appcast open the release's page instead of installing its package. | Target | How a direct build updates | | --- | --- | | macOS | Sparkle, which `build` puts into the app, checks daily, offers the new version, downloads the disk image, checks its signature, installs it and restarts the app | | Windows | WinSparkle, beside the executable, does the same and runs the installer silently, which starts the new version | | Linux, Android | The app reads its appcast, and once the user chooses Update downloads and verifies the signed package and installs it through `pkexec dpkg`, restarting, or Android's package installer; an app from the Arch package or the AppImage, or an unsigned appcast, offers the release's page instead | | iOS | The app asks the App Store, which updates it, and offers its page there | ### Channels The channel decides who updates the app. `-channel` replaces `updates.channel` for one build, so one project builds for each: | Channel | For | The packages | | --- | --- | --- | | `direct` | GitHub, a website, Homebrew casks and winget, which install the release's own files | Update themselves as above | | `store` | The App Store, the Mac App Store, the Microsoft Store, Google Play, Flathub and other stores | Are the [store packages](#store-packages) and carry no updater; iOS asks the App Store, which needs no key | | `none` | Linux distributions and anyone who repackages the app | Check nothing | Stores reject apps that update themselves, so upload a `store` build, never a direct one. A Homebrew cask for a direct app says `auto_updates true`. Wherever the system tells who installed the app, the app checks that too: an Android app from Google Play or another store, a Flatpak or Snap, and a TestFlight build check nothing, as their store updates them. ### Checking from the app `updater.Check(cx)` checks as a *Check for Updates…* menu item does, and shows what it finds; `updater.CheckInBackground(cx)` shows only a newer version, and `updater.CheckDaily(cx)` does that at most once a day. While `automatic` is on, the app checks that way on Linux, Android and iOS as it starts and whenever the user comes back to it; a check that fails, as one offline does, is tried again an hour later. Development builds never update. ## File types `file_types` makes a macOS app open its documents from the Finder, the Dock and Open With: ```yaml file_types: - name: Flow File extensions: [flow] # id: dev.example.hello.flow # . by default; another's ID imports its type # icon: assets/Flow.icns # or a square PNG # role: viewer # editor by default # conforms: [public.json] # public.data by default ``` The app hears the files with `desktop.OnOpenFiles`, which also takes those named on the command line on Windows and Linux. Files that launched the app wait for the first handler: ```go desktop.OnOpenFiles(cx, func(paths []string) { open(paths[0]) }) ``` ## Mini programs ```sh go tool nsgo build -target miniprogram_wechat -run go tool nsgo dev -target miniprogram_alipay ``` Each target writes a project for its platform's developer tool, also in `dist/releases/miniprogram/`, which `-run` and `dev` open; `dev` builds again as you save, and the tool reloads: | Target | Platform | Tool | | --- | --- | --- | | `miniprogram_wechat` | WeChat | [WeChat DevTools](https://developers.weixin.qq.com/miniprogram/dev/devtools/download.html), with its service port on (Settings, Security) | | `miniprogram_qq` | QQ | [QQ's developer tool](https://q.qq.com/wiki/tools/devtool/), on macOS | | `miniprogram_alipay` | Alipay | [Alipay's Mini Program Studio](https://opendocs.alipay.com/mini/ide/download), on macOS | | `miniprogram_douyin` | Douyin and ByteDance's other apps | [Douyin's developer tool](https://developer.open-douyin.com/docs/resource/zh-CN/mini-app/develop/developer-instrument/download/developer-instrument-update-and-download), on macOS | `miniprogram.appid` gives the mini program's AppID on each platform (`wechat`, `qq`, `alipay`, `douyin`); without one, WeChat DevTools previews it with its tourist AppID and the other tools ask for one. The platform's navigation bar shows the app's title; with `custom_navigation`, the app fills the page and its safe area keeps it clear of the menu button. QQ, Alipay and Douyin have not been tried in their tools yet, and Alipay does not yet take back through the app, keyframes or the keyboard's height. Mini programs are experimental. A mini program runs Go apps alone, not TSX, and the platforms hold the app's compressed module to 2 MB: an app of a few screens fits, larger ones do not yet, and `build` says when one does not. Shaders draw nothing there, and web views, videos, native components and the app's font files are not shown yet. ## Sign ```yaml sign: identity: "Apple Development: you@example.com (ABCDE12345)" # signs the macOS app, else ad hoc profile: assets/AppStore.mobileprovision # signs the IPA keystore: assets/release.p12 # signs APKs, with key_alias # widgets_profile: assets/Widgets.mobileprovision # signs the IPA's widget extension # clip_profile: assets/Clip.mobileprovision # signs the IPA's App Clip ``` The keystore's passwords come from `NSGO_KEYSTORE_PASSWORD` and `NSGO_KEY_PASSWORD`. `-team` signs the iPhone's package. ### Notarize Apps downloaded on a Mac open without Gatekeeper's warning once Apple has notarized them. Sign with a Developer ID and name the credentials notarytool keeps in your keychain: ```sh xcrun notarytool store-credentials hello --apple-id you@example.com --team-id ABCDE12345 ``` ```yaml sign: identity: "Developer ID Application: Example Inc (ABCDE12345)" notarize: hello # the keychain profile above; or -notarize hello ``` `build` then signs with the hardened runtime, submits the disk image, waits for Apple's verdict, a few minutes, and staples the ticket to the image and the app; a rejection prints Apple's log. In CI, `NSGO_NOTARY_KEY`, `NSGO_NOTARY_KEY_ID` and `NSGO_NOTARY_ISSUER`, an App Store Connect API key's `.p8` file, ID and issuer, take the profile's place. The hardened runtime denies the microphone, the camera and the calendars unless the app claims them, which `build` does when `apple.info` has `NSMicrophoneUsageDescription`, `NSCameraUsageDescription`, or a calendars or reminders usage description. ## Widgets and Live Activities `build` and `dev` compile the extension that draws the app's widgets and Live Activities into the macOS and iOS packages with Xcode's Swift compiler, with no separate step. On iOS the app and its widgets share an app group: `-run` on an iPhone has Xcode sign both and register the group; for an IPA, enable it for both `` and `.widgets`, and sign the extension with `sign.widgets_profile`. Android 16 shows Live Activities as Live Updates, which need no extension. [Widgets](https://nsgo.dev/docs/widgets.md) and [Live Activities](https://nsgo.dev/docs/live-activities.md) cover their configuration and API. ## App Clips An App Clip is a small part of an iOS app that runs from a link, a code or an NFC tag without installing the app. `app_clip` builds one into the iOS package, from a main package of its own so it links only what it shows: ```yaml app_clip: main: ./clip # the clip's main package, the app's own by default domains: [example.com] # whose links open it # name: Hello Clip # " Clip" by default # id: dev.example.hello.Clip # .Clip by default # minimum: "17.0" # 16.0 by default; 17.0 allows 100 MB when only links open it # notifications: true # ask for notifications for a day # location: true # let the clip confirm where it was opened ``` The clip reads the link that opened it with `mobile.OpenedURL`, which changes as another link opens it again, and `mobile.IsAppClip` says whether the code runs as one. The system downloads a clip of up to 15 MB, or 100 MB from iOS 17 when only links open it, and `build` refuses a larger one; a clip linking `ui` takes about 11 MB. `dev -target ios_simulator -clip https://example.com/order?table=12` runs the clip alone in the simulator, opened from that link, and `-target ios` on an iPhone, printing the link's QR code: list the link in Settings > Developer > Local Experiences and the camera opens the clip from it. Apple signs App Clips only for teams of the Apple Developer Program, not personal teams. For the App Store, enable App Clips for ``, sign the clip with `sign.clip_profile`, and serve the apple-app-site-association file of the domains. The repository's `examples/appclip` is a café whose clip orders at a table. ## Embed in another app `-embed` packages the app for an iOS or Android app of your own: an XCFramework, of dynamic frameworks with `-dynamic`, or an AAR whose `NSGoView` or `NsgoView` shows it. Go desktop programs show apps with `nsgo.NewView`, and web pages with the site's `nsgo.js`, whose host is the page's or the one `web.host` in `nsgo.yaml` names. The repository's `examples/embed-*` show each; [Embedding](https://nsgo.dev/docs/embedding.md) covers the host side. --- Source: https://nsgo.dev/docs/embedding.md # Embedding > Put an NSGo app inside an iOS, Android, desktop or web app of your own. ## Build an embeddable app An embeddable app is a normal app package with a `Root` and a `main`, built with `-embed` for the host's target. Each view runs the app once and is an engine of its own, so a host shows as many views as it likes, each with its own state. ```sh go tool nsgo build -embed -target ios examples/embedded go tool nsgo build -embed -target android_arm64 examples/embedded go tool nsgo build -target web examples/embedded ``` The iOS build writes `.xcframework` and `Resources`; the Android build writes `.aar`; the web build writes a directory with `nsgo.json` and the loader. ## iOS Add the framework to the app target as a static library (do not embed it) and the resources folder as a folder reference, then `import `. Call `NSGoStart()` once as the app launches, before the first view. ```swift import Embedded import UIKit @main final class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ app: UIApplication, didFinishLaunchingWithOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { NSGoStart() return true } } let view = NSGoView(frame: .zero) view.params = ["user": "Ada"] // cx.HostParams() view.dataDirectory = documents.path // where storage lives view.localeIdentifier = "de-CH" // in place of the system's view.setFontScale(1.3) view.overrideUserInterfaceStyle = .dark view.tokens = ["accent": "#cf222e"] // over the app's theme view.handle("time") { _, reply in reply(Date().description, nil) } view.send("message", ["text": "Hello"]) // cx.OnHostEvent("message", …) view.onOpenURL = { url in router.open(url) } view.onError = { message in log(message) } view.register(component: "host.badge") { context in Badge(context) } ``` `NSGoView` starts the app once it is in a window and stops it when it is deallocated or sent `stop()`. Set the launch values before it is in a window; the rest may change at any time. Handlers, components and callbacks run on the main thread, and a reply may come later from any thread. A view controller that forwards the status bar returns the view's `preferredStatusBarStyle` and `prefersStatusBarHidden`. ## Android Add the AAR to the app module and package native libraries extracted, so the runtime finds them: ```kotlin implementation(files("libs/Embedded.aar")) android { packaging { jniLibs { useLegacyPackaging = true } } } ``` Make `dev.oneo.nsgo.NsgoView`s in code or in a layout. Each starts the app in a process of its own as it first gets a size, and `stop()` ends it. Forward the activity's results to every view, and handle configuration changes in the activity so the apps are not restarted. ```java NsgoView app = new NsgoView(context); app.setParams(Map.of("user", "Ada")); app.setStorageDirectory(dir); app.setLocale(Locale.forLanguageTag("de-CH")); app.setFontScale(1.3f); app.setAppearance(NsgoView.APPEARANCE_DARK); app.setTokens(Map.of("accent", "#cf222e"), null); app.handle("time", (data, reply) -> reply.resolve(now())); app.handle("tapped", (data, reply) -> status.setText("tapped " + ((Map) data).get("count"))); app.setOpenUrlHandler(url -> router.open(url)); app.setErrorListener(message -> log(message)); app.registerComponent("host.badge", Badge::new); ``` ```java @Override protected void onActivityResult(int request, int result, Intent data) { super.onActivityResult(request, result, data); for (NsgoView app : apps) app.activityResult(request, result, data); } ``` ## Desktop A Go host runs the platform's event loop and puts each view's `Handle` in its window. `nsgo.NewView` starts the app once the handle is in a window, so settings made before are in its first frame. ```go channel := core.NewHost() channel.SetParams(map[string]any{"user": "Ada"}) channel.Handle("time", func(c *core.HostCall) { c.Resolve(time.Now().Format("15:04:05")) }) channel.HandleOpenURL(func(url string, answer func(bool)) { answer(strings.HasPrefix(url, "https://example.com/orders/")) }) view := nsgo.NewView(app.Root, nsgo.HostChannel(channel), nsgo.DataDir(dataDir)) defer view.Close() view.SetTokens(core.Tokens{Values: map[string]string{"accent": "#cf222e"}}) view.SetLocale("de-CH") view.SetFontScale(1.3) view.SetAppearance(core.AppearanceDark) handle := view.Handle() // an NSView, GtkWidget or child window ``` `view.Host()` sends events to the app and receives its calls, and `nsgo.Component` registers a native component for the host's platform. ## Web The web build is a page of its own, but a page can mount it in an element and be its host. `mount(element, url, options)` returns a player, and `nsgo.json` names the loader, the wasm and the resources relative to itself. ```html
``` Without a `host` option the loader imports the module `nsgo.yaml`'s `web.host` names and awaits its `createHost(player)`. An app in an element or a frame leaves the browser's history and title to the page. `appearance` (`"light"`, `"dark"` or `"system"`), `locale` and `fontScale` stand in for the browser's, as a native host's settings do, and the player's `setAppearance`, `setLocale` and `setFontScale` change them later. `module`, a compiled `WebAssembly.Module` or a promise of one, spares fetching and compiling the wasm, so a page mounting the app many times, or serving the wasm compressed, compiles it once. ```yaml web: host: host.js ``` ## The host channel `core.Host` is the one contract every host implements: launch parameters, events the host sends, and the host's functions the app calls. Values cross as JSON does, so maps become `map[string]any`, numbers `float64`, and the same code works in Swift, Java, Go and JavaScript. - `HostParams` reads the launch parameters once, at start. - `cx.OnHostEvent` hears `Host.Send` and the page's `hostEvent`. - `cx.CallHost` calls a named handler and resolves with its answer; `cx.PostHost` calls one without an answer. - `Host.HandleOpenURL` sees `cx.OpenURL` before the platform, and its answer decides whether the platform opens it too. - `Host.SetParams`, `Send`, `Handle` and `HandleDefault` are safe from any goroutine. On iOS the host sets `params`, installs handlers with `handle`, sends events with `send` and answers `onOpenURL`. On Android it does the same with `setParams`, `handle`, `send` and `setOpenUrlHandler`. On desktop it uses `core.Host`'s methods, and on the web the page's `host` object answers. ## Native components `core.Native` shows a view the host makes: its `Props` reach the host component's `SetProp`, what the component emits reaches `OnEvent`, and a prop whose value is an animated node moves on the frame clock without work for the engine. ```go core.Native{ Component: "host.badge", PropsFunc: func() map[string]any { return map[string]any{"count": count.Get()} }, OnEvent: func(name string, data any) { if name == "tap" { count.Set(0) } }, Style: core.Style{MinWidth: core.Pt(120), MinHeight: core.Pt(28)}, } ``` Register the component in the host: `app.register(component:)` and an `NSGoComponent` on iOS, `registerComponent` and an `NsgoComponent` on Android, `nsgo.Component` on desktop. The web has no native components, so an unknown name shows an empty box there. ## Settings `tokens` overrides the app's theme by role name (`accent`, `inset`, `raised`), in both appearances or in one; `cx.Palette()` sees them. The host also sets the locale, the text size, the appearance and the data directory, and hears the app's failures. `nsgo build -embed` writes no windows, menus or tray icons: those belong to the host. `examples/embed-ios`, `examples/embed-android`, `examples/embed-desktop` and `examples/embed-web` are the same app with each kind of host. ## Next - [Platform services](https://nsgo.dev/docs/services.md): what the app asks the platform for. - [Building and shipping](https://nsgo.dev/docs/building.md): the packages `-embed` makes, and signing them. - [Example: A todo app](https://nsgo.dev/docs/tutorials/todo.md): the app a host embeds. --- Source: https://nsgo.dev/docs/ui.md # Component reference > The most-used components, their variants, states and accessibility. These are `ui`'s components, in NSGo's look and following the theme. This reference starts with the components an app reaches for first; each page starts with the shortest example, then covers its parts, states and accessibility. The gallery covers every component, including those without a page here yet. The [gallery](https://nsgo.dev/gallery/) runs a demo of every component in the browser. ## Actions - [Button](https://nsgo.dev/docs/ui/button.md): a push button, plain or in the accent color, and the icon button. ## Text and input - [Text](https://nsgo.dev/docs/ui/text.md): paragraphs, spans, links, fonts and document text. - [Input](https://nsgo.dev/docs/ui/input.md): the platform's text field, controlled or not. - [Checkbox](https://nsgo.dev/docs/ui/checkbox.md): a check box, checked, unchecked or indeterminate. - [Switch](https://nsgo.dev/docs/ui/switch.md): a setting that is on or off. - [Select](https://nsgo.dev/docs/ui/select.md): one value from a list, as a popup or a sheet. - [Slider](https://nsgo.dev/docs/ui/slider.md): one value or a range along a track. ## Collections - [List](https://nsgo.dev/docs/ui/list.md): rows of any number, built only in view, with selection and activation. - [Scroll](https://nsgo.dev/docs/ui/scroll.md): the platform's scroll view, with paging, snapping, zoom and refresh. ## Navigation - [Tabs](https://nsgo.dev/docs/ui/tabs.md): a tab list, a segmented control and the system tab bar. ## Overlays - [Dialog](https://nsgo.dev/docs/ui/dialog.md): a modal surface, a sheet and the system's alert. - [Menu](https://nsgo.dev/docs/ui/menu.md): a menu from a trigger, a context menu and their items. ## More The gallery covers the rest: tables, charts, the code editor, rich text, the terminal, the dock, the command palette, color and date pickers, toasts and more. Each has a demo there. ## Next - [Components and state](https://nsgo.dev/docs/components.md): how a component returns elements and how signals update them. - [Styles and themes](https://nsgo.dev/docs/styles.md): the theme components take their colors from. - [Unstyled components](https://nsgo.dev/docs/authoring/base.md): the same controls without a look, for a look of your own. - [Accessibility](https://nsgo.dev/docs/accessibility.md): labels, roles and values for every control. --- Source: https://nsgo.dev/docs/ui/button.md # Button > A push button, its variants and sizes, and the icon button. `ui.Button` is a push button in `ui`'s look, with the variants and sizes a window needs. `OnPress` runs on a completed press. ```go ui.Button{Text: "Save", OnPress: save} ui.Button{Text: "Cancel", Variant: ui.ButtonSecondary, OnPress: cancel} ``` ## Variants and sizes `ui.Button.Variant` is `ButtonPrimary` (the default), `ButtonSecondary`, `ButtonOutline` or `ButtonGhost`. `Size` is `SizeDefault` (28 points), `SizeSmall` (24) or `SizeLarge` (32). ```go ui.Button{Text: "Delete", Variant: ui.ButtonOutline, OnPress: remove} ui.Button{Text: "Continue", Size: ui.SizeLarge, OnPress: next} ui.Button{Text: "Save", Icon: lucide.Check, Flex: 1, OnPress: save} ``` `Color` fills the button with a color of the app's instead of a variant's, and `Tint` its icon. `Flex` gives it a share of a row; `TestID` names it for tests. A button of an icon alone is `ui.IconButton`, with `Variant` `IconButtonGhost` or `IconButtonRaised`, a `Size` in points and a `Label` for assistive technology. A `ui.Button` with `Icon` and no `Text` is a square too, and needs `Label`. ```go ui.IconButton{Icon: lucide.Trash, Label: "Delete", Variant: ui.IconButtonGhost, OnPress: remove} ``` ## Content and states `ui.Button` shows `Text`, an optional leading `Icon`, or both. ```go ui.Button{Text: "Saving…", Disabled: saving.Get, OnPress: save} ``` `Disabled` takes a function, so the button follows a signal: a disabled button takes neither presses nor the keyboard focus, and its look is the disabled state, not a color the app picks. ## Accessibility The button reports `RoleButton`, its text as its name, and `Hint` as what pressing it does. `Label` replaces the text for assistive technology. An icon-only button without `Label` is unreadable, so name it. Return and Space press a focused button on macOS, the web and Apple TV; a phone's screen reader presses it as a click. ## Platform The size is the same on every device. The button is a view drawn the same way everywhere, not a platform button; presses, focus and accessibility come from the platform. ## Related - [Forms](https://nsgo.dev/docs/forms.md): buttons in a form and validation. - [Styles and themes](https://nsgo.dev/docs/styles.md): the theme a button takes its colors from. - [Accessibility](https://nsgo.dev/docs/accessibility.md): roles, labels and focus. - The gallery's Button page. - [Unstyled components](https://nsgo.dev/docs/authoring/base.md#button): `base.Button`, for a button of a look of your own. --- Source: https://nsgo.dev/docs/ui/text.md # Text > Paragraphs, spans, links, fonts and documents. `core.Text` is one paragraph typeset by the platform's text engine. `ui.Text` and `ui.Textf` are helpers for a line in `ui`'s type; the element is still a `core.Text`, so it takes `Style` and `Spans` too. ```go ui.Text("Settings", 20, ui.Bold, colors.Text) ui.Textf(func() string { return fmt.Sprintf("%d files", n.Get()) }, 13, ui.Regular, colors.Muted) ``` ## Content `Content` is fixed and `ContentFunc` follows a signal. `MaxLines` limits the lines and `EllipsizeMode` cuts the rest at its head, middle or tail; `Style.WhiteSpaceNoWrap` keeps a paragraph on one line. `TextAlign`, `TextTransform`, `LineHeight` and `LetterSpacing` style the paragraph, and `OnTextLayout` reports its lines after layout. ```go core.Text{ContentFunc: func() string { return fmt.Sprintf("Pressed %d times", count.Get()) }, MaxLines: 2, EllipsizeMode: core.EllipsizeTail, Style: core.Style{FontSize: 15}} ``` ## Spans and links `Spans` makes a text of runs, each with a style of its own. A span with `OnPress` is a link, and its `Background` and `CornerRadius` draw behind its characters on every line. `SpansFunc` gives the runs again when what it reads changes, for highlighted code or a changing sentence. ```go core.Text{Style: core.Style{FontSize: 15}, Spans: []core.Span{ {Content: "Read the "}, {Content: "guide", Style: core.Style{TextDecoration: core.TextDecorationUnderline}, OnPress: openGuide}, {Content: " before you start."}, }} ``` `Attachment` puts a view in a line, in place of the span's content: an icon, an image or a small button. It is laid out at its own size, wraps with the words and takes its own presses. ## Fonts and style `FontFamily` names a family or a CSS list. `FontSize`, `FontWeight`, `FontStyle`, `FontVariant`, `LineHeight`, `LetterSpacing`, `TextDecoration` and `TextShadowColor` and its offset and radius style the text. `core.FontMonospace` is the platform's fixed-pitch family. ```go core.Text{Content: "fn main() {}", Style: core.Style{FontFamily: core.FontMonospace, FontSize: 13}} ``` Register packaged fonts with `nsgo.Fonts` before the first view, or register a face while the app runs with `cx.RegisterFont`. A text prepared before a font arrives is prepared again. ## Documents `base/textview.TextView` shows Markdown or HTML with the same primitives: headings, paragraphs, lists, tables, code, footnotes, alerts, images, formulas and links. `textview.Parse` reads Markdown and `textview.ParseHTML` HTML, and `Options.Streaming` repairs a document still arriving. ```go textview.TextView{Markdown: "## Notes\n\nA **native** document.", Styles: ui.TextViewStyles(cx), OnLinkPress: func(e textview.LinkPressEvent) { open(e.URL) }} ``` ## Selection and accessibility `SelectableOn` lets the user select and copy a text. A `core.View` with `Selectable` makes its texts select together in document order. A `Role` such as `RoleHeader` tells assistive technology what the text is, and `Label` replaces it when the words are not the label. ## Platform The platform's text engine lays the text out, so line breaks, fonts and input methods follow it. CJK line breaking, emoji and right-to-left text follow the system. The app can read the text size with `cx.Env().FontScale()`, and a text follows it unless `NoFontScaling` is set. ## Related - [Text](https://nsgo.dev/docs/text.md): the full guide to text, spans, fonts and documents. - [Images and icons](https://nsgo.dev/docs/images.md): inline images and icons. - [Accessibility](https://nsgo.dev/docs/accessibility.md): headings, labels and selection. - The gallery's Text and TextView pages. --- Source: https://nsgo.dev/docs/ui/input.md # Input > The platform's text field, controlled or not, with focus and the software keyboard. `ui.Input` is the platform's text field in `ui`'s look, one line unless `Multiline`. It has `base.Input`'s fields, as every styled field does; `ui.Field` is the same field in a borderless rim, for a field the app sizes itself. ```go name := core.NewSignal(cx, "") ui.Input{ Value: name.Get, OnChange: name.Set, Placeholder: "Your name", TestID: "name", } ``` ## Controlled and uncontrolled With `Value`, the field is controlled: the app holds the value and updates it in `OnChange`. With `DefaultValue`, the field keeps its own. A controlled field can be cleared by setting its value to `""`. ```go draft := core.NewSignal(cx, "") ui.Input{Value: draft.Get, OnChange: draft.Set, Placeholder: "New task", OnSubmit: func(string) { add(); draft.Set("") }} ``` ## Fields `Multiline` wraps and grows with its text unless its style sizes it. `Masked` hides the characters, `Readonly` and `Disabled` stop edits, and `MaxLength` caps the UTF-16 units the user enters. `Placeholder` can be a function with `PlaceholderFunc`. ```go ui.Input{Masked: true, Value: password.Get, OnChange: password.Set, AutoComplete: core.AutoCompletePassword} ``` `KeyboardType` picks a software keyboard (`KeyboardEmail`, `KeyboardNumber`, `KeyboardURL` and the rest), `ReturnKeyType` labels its return key, and `SubmitBehavior` says whether Return submits or breaks a multiline line. `AutoCapitalize`, `NoAutoCorrect` and `AutoComplete` tune the system's help. ## Focus and events `Ref` focuses and blurs the field. `OnFocus` and `OnBlur` follow the focus, `OnSelectionChange` and `OnCaretMove` the selection and the caret, `OnKeyPress` each typed key, `OnPaste` what is pasted and `OnCompositionChange` an input method composing. ```go email := &core.Ref{} ui.Input{Ref: email, Placeholder: "you@example.com"} email.Focus() ``` The software keyboard covers the bottom of a phone. Put `core.EnvKeyboard` on the bottom padding or margin of the content so it stays above the keyboard; the move animates with it. `cx.DismissKeyboard()` closes the keyboard, and `cx.Env().KeyboardHeight()` reads its height. ## Validation The app owns the rules. A signal holds the error, `OnBlur` or `OnSubmit` sets it, and a message under the field shows it ([Forms](https://nsgo.dev/docs/forms.md#validation)). ```go err := core.NewSignal(cx, "") core.View{Style: core.Style{Gap: 4}, Children: []core.Node{ ui.Input{Value: email.Get, OnChange: email.Set, Label: "Email", OnSubmit: func(string) { if !strings.Contains(email.Get(), "@") { err.Set("Enter an email address.") } else { err.Set("") } }}, ui.Textf(err.Get, 12, ui.Regular, colors.Danger), }} ``` ## Accessibility The field reports `RoleTextField` and reads its `Label`; `Placeholder` is not a label, so give a field with no visible label a `Label`. `Readonly` and `Disabled` are reported, and `OnSubmit` is the keyboard's action. ## Platform iOS, Android, Windows and Linux use their native fields; the web uses an `` or `