Authoring Extensions
Extensions can provide ordinary Go components, retained native components, or asynchronous native services. Editor, Markdown, and Terminal are optional native component packages; Updater is an optional service. All use the same public registration boundary. Third-party extensions need no changes to quickgui-host.
#Choose an extension type
| What you are adding | What you ship |
|---|---|
| Components or application logic | A Go module |
| A native service or optional backend | A Go package, a manifest, and a native library |
| Retained native UI | A component SDK library and Go bindings |
Consumers import your Go package. The CLI finds the manifest and bundles the matching native artifact.
#Create an extension project
quickgui init-extension my-components --type go
quickgui init-extension my-service --type zig
quickgui init-extension my-service --type rustgo is the default and scaffolds a reusable component. zig and rust scaffold a native service with a typed Go wrapper, a manifest, and an npm artifact package. Every template includes cmd/demo and quickgui.toml:
cd my-service
bun run devUse --module github.com/acme/my-service for the Go module and --npm-package @acme/extension-my-service for the artifact package. Zig templates need Zig 0.16.x. From this checkout, run bun packages/cli/src/cli.ts init-extension until the CLI is published.
#Share a component
Export component functions from your own module. This package needs no native manifest.
package notice
import "github.com/egoist/quickgui/go/ui"
func Notice(message string, styles ...ui.StyleBuilder) *ui.Element {
return ui.Text(
message,
).
Padding(12).
BackgroundColor("#eff6ff").
TextColor("#1e40af").
BorderRadius(8).
Style(ui.Style().
Merge(styles...))
}Publish with Go version tags. Consumers add it with go get. Importing terminal or updater still opts an app into those native packages, so keep optional integrations in separate packages.
#Lay out a native extension
The first-party packages in extensions/ are each both a Go module and an npm package. Keep Rust in src/ and TypeScript bindings in js/. You can use the same layout for your own combined package:
Cargo.toml
go.mod
badge.go
package.json
quickgui.extension.json
src/
lib.rs
js/
index.ts
lib/
darwin-arm64/libquickgui_acme_badge.dylibC, C++, Rust, and other languages with a C ABI can implement extensions. Rust component libraries depend on quickgui-extension-sdk, not on the renderer or quickgui-host. Implement Component and ComponentFactory; ComponentRuntime supplies the lifecycle table. The extension owns its name, component names, properties, and release version.
#Declare dependencies
Place this manifest beside the imported Go package’s source:
{
"schema": 1,
"name": "acme-echo",
"abi": 1,
"package": "@acme/extension-echo",
"version": "1.0.0",
"library": "quickgui_acme_echo"
}Names start with a lowercase letter and contain at most 64 lowercase letters, digits, or hyphens. host is reserved. The library field is quickgui_ plus the name with hyphens replaced by underscores.
The Go package records the requirement and exposes a typed call:
package echo
import (
"encoding/json"
"github.com/egoist/quickgui/go/host"
"github.com/egoist/quickgui/go/native"
)
func init() {
host.RequireExtension("acme-echo", "1.0.0")
}
func Send(message string, done func(string, error)) {
native.InvokeExtension("acme-echo", "echo", message, func(raw string, err error) {
var reply string
if err == nil {
err = json.Unmarshal([]byte(raw), &reply)
}
done(reply, err)
})
}Call operations after native.Run is ready. For a persistent service, native.OpenExtension(name, options, changed, ready) returns a session. Close it on the UI goroutine when the owner is disposed.
The CLI discovers manifests through go list -deps. Both the manifest and RequireExtension are required: one controls bundling, the other controls loading.
#Implement the native contract
Export quickgui_extension_v1 with C linkage and a descriptor containing your name, independent version, ABI version 1, and one of the generic function tables. A component table has create, update, render, event, and destroy; a service table has invoke and shutdown.
A component retains an opaque instance and renders the SDK's primitive tree. Stable node keys preserve focus, selection, and scrolling. Virtual lists request only visible rows plus overscan. Input returns to the package; emitted events reach its Go binding. Calls on an instance are serialized on the native UI thread. Move slow work to a worker and wake only that instance when its data changes. Return unchanged for equivalent props, and release state and wake handles on destruction.
A Go wrapper can mount any registered component:
package badge
import (
"github.com/egoist/quickgui/go/host"
"github.com/egoist/quickgui/go/ui"
)
func init() { host.RequireExtension("acme-badge", "1.0.0") }
func View(label string) *ui.Element {
return ui.ExtensionComponent("acme-badge", "badge", map[string]any{
"label": label,
}, ui.Props{})
}See the independently compiled C counter for a complete component descriptor and lifecycle. Component frames are bounded to 16 MiB; frontend property JSON is bounded to 12 MiB.
For a service, copy any borrowed request bytes before keeping them, return quickly, and reply later.
| Request | What the extension does |
|---|---|
A one-shot method such as echo | Reply once |
start | Open a session and keep its event sink |
A command such as send | Find that session and reply to this command |
stop | Cancel that session |
shutdown | Cancel every session without blocking |
Request and reply payloads are each limited to 64 KiB. The C echo implementation is a complete one-shot example.
#Automatic registration
The app loads each selected library at startup and checks its name, version, and ABI. Conflicting versions of the same name fail. Images stay loaded for the process lifetime; your shutdown function must cancel remaining sessions.
#Build and package artifacts
Build the native library for each host you support and stage it under lib/<platform>-<arch>/. Publish that folder as an npm package whose version matches the manifest.
{
"name": "@acme/extension-echo",
"version": "1.0.0",
"files": ["js", "lib", "quickgui.extension.json", "README.md"],
"exports": { ".": "./js/index.ts", "./package.json": "./package.json" }
}If the npm package includes TypeScript bindings, export js/index.ts. Declare @quickgui/* runtime packages and Solid as peer dependencies, with development copies in devDependencies, not runtime dependencies. Keep the package README to build instructions; publish the user guide on your website.
Helper files go next to the library. Declare their basenames in a manifest resources object keyed by darwin, linux, or windows.
#Test and distribute
Consumers go get your module and run quickgui dev. The CLI fetches the matching npm package, or you can set QUICKGUI_EXTENSION_DIR to a local artifact directory while developing.
Confirm that an app without the import does not bundle the extension, that adding the import includes it, and that closing sessions stops events. End users receive the packaged app; they do not need Bun or a native toolchain to run it.
