native: move Zig sources into packages/native (#1391)

Separate the native implementation from the TypeScript bindings. Keep
core on
the public bindings and keep the current native artifact packages and
release
workflows.

packages/native owns the Zig source, tests, benches, and vendor updates.
The
package is private and unpublished for now Core still packages the
@opentui/core-* artifacts. build:native, test:native, and bench:native
still
work from core and delegate to native.

Expose a Zig import root and a no-JS hello example for proof of concept,
not a focus point, just a smoke test.
This commit is contained in:
Simon Klee
2026-08-20 11:34:03 +02:00
committed by GitHub
parent 2417a2c0b7
commit 6dec16a77c
301 changed files with 366 additions and 114 deletions
+1 -1
View File
@@ -40,7 +40,7 @@ jobs:
- name: Restore Zig cache
uses: actions/cache@v5
with:
path: .zig-cache
path: packages/native/.zig-cache
key: zig-v1-${{ runner.os }}-${{ env.ZIG_VERSION }}-${{ github.run_id }}-${{ github.job }}
restore-keys: zig-v1-${{ runner.os }}-${{ env.ZIG_VERSION }}-
+6 -7
View File
@@ -40,7 +40,7 @@ jobs:
- name: Restore Zig cache
uses: actions/cache@v5
with:
path: .zig-cache
path: packages/native/.zig-cache
key: zig-v1-${{ runner.os }}-${{ env.ZIG_VERSION }}-${{ github.run_id }}-${{ github.job }}
restore-keys: zig-v1-${{ runner.os }}-${{ env.ZIG_VERSION }}-
@@ -53,8 +53,8 @@ jobs:
bun run build:native --all
- name: Build custom target without Ghostty VT
working-directory: packages/core/src/zig
run: zig build -Dtarget=x86_64-freebsd -Doptimize=ReleaseFast
working-directory: packages/native
run: zig build -Dlibrary-target=x86_64-freebsd -Doptimize=ReleaseFast
- name: Upload native artifacts
uses: actions/upload-artifact@v7
@@ -105,7 +105,7 @@ jobs:
if: runner.os != 'Windows'
uses: actions/cache@v5
with:
path: .zig-cache
path: packages/native/.zig-cache
key: zig-v1-${{ runner.os }}-${{ env.ZIG_VERSION }}-${{ github.run_id }}-${{ github.job }}
restore-keys: zig-v1-${{ runner.os }}-${{ env.ZIG_VERSION }}-
@@ -125,9 +125,8 @@ jobs:
ls -la packages/core/node_modules/@opentui/core-${{ matrix.platform }}/
- name: Run native tests
run: |
cd packages/core/src/zig
zig build test --summary all
working-directory: packages/core
run: bun run test:native
- name: Build TypeScript library
run: |
+6 -6
View File
@@ -131,12 +131,12 @@ jobs:
mkdir -p artifacts/example-notices
cp LICENSE artifacts/example-notices/LICENSE
cp packages/core/THIRD_PARTY_LICENSES/GHOSTTY artifacts/example-notices/LICENSE-GHOSTTY
cp packages/core/src/zig/vendor/wuffs/LICENSE artifacts/example-notices/LICENSE-WUFFS
cp packages/core/src/zig/vendor/stb/LICENSE artifacts/example-notices/LICENSE-STB
cp packages/core/src/zig/vendor/libwebp/COPYING artifacts/example-notices/LICENSE-LIBWEBP
cp packages/core/src/zig/vendor/libwebp/PATENTS artifacts/example-notices/PATENTS-LIBWEBP
cp packages/core/src/zig/vendor/libwebp/AUTHORS artifacts/example-notices/AUTHORS-LIBWEBP
cp packages/core/src/zig/vendor/lcms2/LICENSE artifacts/example-notices/LICENSE-LCMS2
cp packages/native/src/vendor/wuffs/LICENSE artifacts/example-notices/LICENSE-WUFFS
cp packages/native/src/vendor/stb/LICENSE artifacts/example-notices/LICENSE-STB
cp packages/native/src/vendor/libwebp/COPYING artifacts/example-notices/LICENSE-LIBWEBP
cp packages/native/src/vendor/libwebp/PATENTS artifacts/example-notices/PATENTS-LIBWEBP
cp packages/native/src/vendor/libwebp/AUTHORS artifacts/example-notices/AUTHORS-LIBWEBP
cp packages/native/src/vendor/lcms2/LICENSE artifacts/example-notices/LICENSE-LCMS2
# Create separate zips for each platform's examples
- name: Package examples - darwin-x64
+2 -2
View File
@@ -46,7 +46,7 @@ jobs:
if: runner.os != 'Windows'
uses: actions/cache@v5
with:
path: .zig-cache
path: packages/native/.zig-cache
key: zig-v1-${{ runner.os }}-0.16.0-${{ github.run_id }}-${{ github.job }}
restore-keys: zig-v1-${{ runner.os }}-0.16.0-
@@ -97,7 +97,7 @@ jobs:
- name: Restore Zig cache
uses: actions/cache@v5
with:
path: .zig-cache
path: packages/native/.zig-cache
key: zig-v1-${{ runner.os }}-0.16.0-${{ github.run_id }}-${{ github.job }}
restore-keys: zig-v1-${{ runner.os }}-0.16.0-
+7 -7
View File
@@ -38,7 +38,7 @@ jobs:
- name: Restore Zig cache
uses: actions/cache@v5
with:
path: .zig-cache
path: packages/native/.zig-cache
key: zig-v1-${{ runner.os }}-${{ env.ZIG_VERSION }}-${{ github.run_id }}-${{ github.job }}
restore-keys: zig-v1-${{ runner.os }}-${{ env.ZIG_VERSION }}-
@@ -107,12 +107,12 @@ jobs:
mkdir -p artifacts/native-notices
cp LICENSE artifacts/native-notices/LICENSE
cp packages/core/THIRD_PARTY_LICENSES/GHOSTTY artifacts/native-notices/LICENSE-GHOSTTY
cp packages/core/src/zig/vendor/wuffs/LICENSE artifacts/native-notices/LICENSE-WUFFS
cp packages/core/src/zig/vendor/stb/LICENSE artifacts/native-notices/LICENSE-STB
cp packages/core/src/zig/vendor/libwebp/COPYING artifacts/native-notices/LICENSE-LIBWEBP
cp packages/core/src/zig/vendor/libwebp/PATENTS artifacts/native-notices/PATENTS-LIBWEBP
cp packages/core/src/zig/vendor/libwebp/AUTHORS artifacts/native-notices/AUTHORS-LIBWEBP
cp packages/core/src/zig/vendor/lcms2/LICENSE artifacts/native-notices/LICENSE-LCMS2
cp packages/native/src/vendor/wuffs/LICENSE artifacts/native-notices/LICENSE-WUFFS
cp packages/native/src/vendor/stb/LICENSE artifacts/native-notices/LICENSE-STB
cp packages/native/src/vendor/libwebp/COPYING artifacts/native-notices/LICENSE-LIBWEBP
cp packages/native/src/vendor/libwebp/PATENTS artifacts/native-notices/PATENTS-LIBWEBP
cp packages/native/src/vendor/libwebp/AUTHORS artifacts/native-notices/AUTHORS-LIBWEBP
cp packages/native/src/vendor/lcms2/LICENSE artifacts/native-notices/LICENSE-LCMS2
# Create separate zips for each platform's native binaries
- name: Package native binaries - darwin-x64
+1 -1
View File
@@ -44,7 +44,7 @@ jobs:
if: runner.os != 'Windows'
uses: actions/cache@v5
with:
path: .zig-cache
path: packages/native/.zig-cache
key: zig-v1-${{ runner.os }}-0.16.0-${{ github.run_id }}-${{ github.job }}
restore-keys: zig-v1-${{ runner.os }}-0.16.0-
+1 -1
View File
@@ -45,7 +45,7 @@ jobs:
if: runner.os != 'Windows'
uses: actions/cache@v5
with:
path: .zig-cache
path: packages/native/.zig-cache
key: zig-v1-${{ runner.os }}-0.16.0-${{ github.run_id }}-${{ github.job }}
restore-keys: zig-v1-${{ runner.os }}-0.16.0-
+3 -3
View File
@@ -46,7 +46,7 @@ jobs:
if: runner.os != 'Windows'
uses: actions/cache@v5
with:
path: .zig-cache
path: packages/native/.zig-cache
key: zig-v1-${{ runner.os }}-0.16.0-${{ github.run_id }}-${{ github.job }}
restore-keys: zig-v1-${{ runner.os }}-0.16.0-
@@ -94,7 +94,7 @@ jobs:
- name: Restore Zig cache
uses: actions/cache@v5
with:
path: .zig-cache
path: packages/native/.zig-cache
key: zig-v1-${{ runner.os }}-0.16.0-${{ github.run_id }}-${{ github.job }}
restore-keys: zig-v1-${{ runner.os }}-0.16.0-
@@ -142,7 +142,7 @@ jobs:
- name: Restore Zig cache
uses: actions/cache@v5
with:
path: .zig-cache
path: packages/native/.zig-cache
key: zig-v1-${{ runner.os }}-0.16.0-${{ github.run_id }}-${{ github.job }}
restore-keys: zig-v1-${{ runner.os }}-0.16.0-
+1 -1
View File
@@ -38,7 +38,7 @@ jobs:
- name: Restore Zig cache
uses: actions/cache@v5
with:
path: .zig-cache
path: packages/native/.zig-cache
key: zig-v1-${{ runner.os }}-0.16.0-${{ github.run_id }}-${{ github.job }}
restore-keys: zig-v1-${{ runner.os }}-0.16.0-
+1 -1
View File
@@ -39,7 +39,7 @@ jobs:
- name: Restore Zig cache
uses: actions/cache@v5
with:
path: .zig-cache
path: packages/native/.zig-cache
key: zig-v1-${{ runner.os }}-0.16.0-${{ github.run_id }}-${{ github.job }}
restore-keys: zig-v1-${{ runner.os }}-0.16.0-
+1
View File
@@ -5,6 +5,7 @@
"sortPackageJson": false,
"ignorePatterns": [
"**/.zig-cache/**",
"**/zig-pkg/**",
"packages/core/src/lib/tree-sitter/default-parsers.ts",
"packages/web/public/recordings/*.json"
]
+1
View File
@@ -18,6 +18,7 @@ bun create tui
This monorepo contains the following packages:
- [`@opentui/native`](packages/native) - Private workspace package containing the Zig implementation, tests, and benchmarks.
- [`@opentui/core`](packages/core) - TypeScript bindings for OpenTUI's native Zig core, with an imperative API and all primitives.
- [`@opentui/three`](packages/three) - Three.js WebGPU renderer for OpenTUI.
- [`@opentui/solid`](packages/solid) - The SolidJS reconciler for OpenTUI.
+6
View File
@@ -92,6 +92,10 @@
"solid-js",
],
},
"packages/native": {
"name": "@opentui/native",
"version": "0.5.4",
},
"packages/qrcode": {
"name": "@opentui/qrcode",
"version": "0.5.4",
@@ -560,6 +564,8 @@
"@opentui/keymap": ["@opentui/keymap@workspace:packages/keymap"],
"@opentui/native": ["@opentui/native@workspace:packages/native"],
"@opentui/qrcode": ["@opentui/qrcode@workspace:packages/qrcode"],
"@opentui/react": ["@opentui/react@workspace:packages/react"],
+2 -2
View File
@@ -6,7 +6,7 @@
"packages/solid/examples"
],
"scripts": {
"build": "cd packages/core && bun run build && cd ../qrcode && bun run build && cd ../three && bun run build && cd ../solid && bun run build && cd ../react && bun run build && cd ../keymap && bun run build && cd ../ssh && bun run build",
"build": "cd packages/native && bun run build && cd ../core && bun scripts/build.ts --native --skip-zig && bun run build:lib && cd ../qrcode && bun run build && cd ../three && bun run build && cd ../solid && bun run build && cd ../react && bun run build && cd ../keymap && bun run build && cd ../ssh && bun run build",
"build:examples": "cd packages/examples && bun run build",
"clean": "bun scripts/clean.ts",
"examples:node": "node packages/examples/scripts/run-node26.mjs",
@@ -25,7 +25,7 @@
"publish:keymap": "cd packages/keymap && bun run publish",
"publish:ssh": "cd packages/ssh && bun run publish",
"prepare-release": "bun scripts/prepare-release.ts",
"test": "bun run --filter '@opentui/core' --filter '@opentui/qrcode' --filter '@opentui/three' --filter '@opentui/solid' --filter '@opentui/react' --filter '@opentui/keymap' --filter '@opentui/ssh' --if-present test"
"test": "bun run --filter '@opentui/native' --filter '@opentui/core' --filter '@opentui/qrcode' --filter '@opentui/three' --filter '@opentui/solid' --filter '@opentui/react' --filter '@opentui/keymap' --filter '@opentui/ssh' --if-present test"
},
"devDependencies": {
"oxfmt": "0.41.0",
+1 -5
View File
@@ -1,8 +1,4 @@
# Zig build cache
.zig-cache
.node-test
.node-benchmark
ffi-fast-path-paired.json
zig-out
src/zig/lib
src/zig/zig-pkg
src/zig/
+1 -1
View File
@@ -40,7 +40,7 @@ Run native performance benchmarks:
bun run bench:native
```
See [src/zig/bench.zig](src/zig/bench.zig) for available options like `--filter` and `--mem`.
See [the native benchmark runner](../native/src/bench.zig) for options such as `--filter` and `--mem`.
NativeSpanFeed TypeScript benchmarks:
+10
View File
@@ -48,6 +48,16 @@ bun run test:native -Dtest-filter="test name"
bun run bench:native
```
The Zig implementation, tests, and benchmarks live in `packages/native`. Run direct Zig commands and the no-JavaScript
example from that package:
```bash
cd packages/native
zig build test --summary all
cd examples/hello
zig build run
```
## Local Development Linking
Link your local OpenTUI to another project:
+4 -4
View File
@@ -24,9 +24,9 @@
"build:lib": "bun scripts/build.ts --lib",
"build:native": "bun scripts/build.ts --native",
"build:native:dev": "bun scripts/build.ts --native --dev",
"vendor:update:images": "sh src/zig/vendor/update.sh",
"test:native": "cd src/zig && zig build test --summary all",
"bench:native": "cd src/zig && zig build bench -Dbench-optimize=ReleaseFast --",
"vendor:update:images": "cd ../native && sh src/vendor/update.sh",
"test:native": "cd ../native && bun run test",
"bench:native": "cd ../native && bun run bench --",
"bench:js": "bun src/benchmark/js-benchmark.ts",
"bench:js:node": "bun scripts/bench-js-node.ts",
"bench:layout": "bun src/benchmark/layout-benchmark.ts",
@@ -47,7 +47,7 @@
"test:standalone": "bun scripts/standalone-test.ts",
"test:js": "bun test --path-ignore-patterns='src/zig/zig-pkg/**'",
"test:js:node": "bun scripts/test-node.ts",
"test": "bun run test:native && bun run test:js"
"test": "bun run test:js"
},
"license": "MIT",
"devDependencies": {
+10 -8
View File
@@ -41,6 +41,7 @@ interface BunBuildOptions {
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
const rootDir = resolve(__dirname, "..")
const nativeRoot = resolve(rootDir, "../native")
const licensePath = path.resolve(__dirname, "../../../LICENSE")
const ghosttyLicensePath = path.resolve(rootDir, "THIRD_PARTY_LICENSES/GHOSTTY")
const packageJson: PackageJson = JSON.parse(readFileSync(join(rootDir, "package.json"), "utf8"))
@@ -51,6 +52,7 @@ const buildNative = args.find((arg) => arg === "--native")
const isDev = args.includes("--dev")
const buildAll = args.includes("--all") // Build for all platforms
const gpaSafeStats = args.includes("--gpa-safe-stats")
const skipZig = args.includes("--skip-zig")
const getHostVariant = (): Variant => {
const hostVariant = variants.find((variant) => variant.platform === process.platform && variant.arch === process.arch)
@@ -187,14 +189,14 @@ if (buildNative) {
zigArgs.push("-Dgpa-safe-stats=true")
}
runCommand("zig", zigArgs, join(rootDir, "src", "zig"), "Error: Zig build failed")
if (!skipZig) runCommand("zig", zigArgs, nativeRoot, "Error: Zig build failed")
const variantsToPackage = buildAll ? variants : [getHostVariant()]
for (const { platform, arch, abi } of variantsToPackage) {
const nativeName = `${packageJson.name}-${platform}-${arch}${abi ? `-${abi}` : ""}`
const nativeDir = join(rootDir, "node_modules", nativeName)
const libDir = join(rootDir, "src", "zig", "lib", getZigTarget(platform, arch, abi))
const libDir = join(nativeRoot, "lib", getZigTarget(platform, arch, abi))
rmSync(nativeDir, { recursive: true, force: true })
mkdirSync(nativeDir, { recursive: true })
@@ -279,12 +281,12 @@ export default module.default
if (existsSync(licensePath)) copyFileSync(licensePath, join(nativeDir, "LICENSE"))
for (const [source, destination] of [
[ghosttyLicensePath, "LICENSE-GHOSTTY"],
[join(rootDir, "src", "zig", "vendor", "wuffs", "LICENSE"), "LICENSE-WUFFS"],
[join(rootDir, "src", "zig", "vendor", "stb", "LICENSE"), "LICENSE-STB"],
[join(rootDir, "src", "zig", "vendor", "libwebp", "COPYING"), "LICENSE-LIBWEBP"],
[join(rootDir, "src", "zig", "vendor", "libwebp", "PATENTS"), "PATENTS-LIBWEBP"],
[join(rootDir, "src", "zig", "vendor", "libwebp", "AUTHORS"), "AUTHORS-LIBWEBP"],
[join(rootDir, "src", "zig", "vendor", "lcms2", "LICENSE"), "LICENSE-LCMS2"],
[join(nativeRoot, "src", "vendor", "wuffs", "LICENSE"), "LICENSE-WUFFS"],
[join(nativeRoot, "src", "vendor", "stb", "LICENSE"), "LICENSE-STB"],
[join(nativeRoot, "src", "vendor", "libwebp", "COPYING"), "LICENSE-LIBWEBP"],
[join(nativeRoot, "src", "vendor", "libwebp", "PATENTS"), "PATENTS-LIBWEBP"],
[join(nativeRoot, "src", "vendor", "libwebp", "AUTHORS"), "AUTHORS-LIBWEBP"],
[join(nativeRoot, "src", "vendor", "lcms2", "LICENSE"), "LICENSE-LCMS2"],
] as const) {
if (!existsSync(source)) throw new Error(`Required native license file is missing: ${source}`)
copyFileSync(source, join(nativeDir, destination))
+10 -2
View File
@@ -40,7 +40,10 @@ const treeSitterTestDataPaths = [
const treeSitterAssetsDir = "src/lib/tree-sitter/assets"
const audioFixturesDir = "src/tests/fixtures/audio"
const imageFixturesDir = "src/tests/fixtures/images"
const iccFixturesDir = "src/zig/tests/fixtures"
const iccFixturesDir = "../native/src/tests/fixtures"
const stagedNativeFixturesRoot = resolve(outDir, iccFixturesDir)
const stagedNativeRoot = resolve(packageRoot, "native")
let stagedNativeRootOwned = false
const nodeTestTimeoutMs = 30_000
const nodeProcessTimeoutMs = 10 * 60_000
const nodePath = requireNode26()
@@ -206,10 +209,14 @@ try {
}
if (exitCode === 0) {
if (existsSync(stagedNativeRoot)) {
throw new Error(`Refusing to replace existing native fixture staging directory: ${stagedNativeRoot}`)
}
cpSync(resolve(packageRoot, treeSitterAssetsDir), resolve(outDir, treeSitterAssetsDir), { recursive: true })
cpSync(resolve(packageRoot, audioFixturesDir), resolve(outDir, audioFixturesDir), { recursive: true })
cpSync(resolve(packageRoot, imageFixturesDir), resolve(outDir, imageFixturesDir), { recursive: true })
cpSync(resolve(packageRoot, iccFixturesDir), resolve(outDir, iccFixturesDir), { recursive: true })
stagedNativeRootOwned = true
cpSync(resolve(packageRoot, iccFixturesDir), stagedNativeFixturesRoot, { recursive: true })
for (const dataPath of treeSitterTestDataPaths) {
mkdirSync(dataPath, { recursive: true })
}
@@ -253,6 +260,7 @@ try {
}
} finally {
rmSync(outDir, { recursive: true, force: true })
if (stagedNativeRootOwned) rmSync(stagedNativeRoot, { recursive: true, force: true })
}
process.exit(exitCode)
+2 -2
View File
@@ -71,8 +71,8 @@ export async function runBenchmarkCli(args: readonly string[], dependencies: Cli
}
function readZigVersion(): string {
const sourceZigDirectory = fileURLToPath(new URL("../zig", import.meta.url))
const zigDirectory = existsSync(sourceZigDirectory) ? sourceZigDirectory : resolve("src/zig")
const sourceZigDirectory = fileURLToPath(new URL("../../../native", import.meta.url))
const zigDirectory = existsSync(sourceZigDirectory) ? sourceZigDirectory : resolve("../native")
const child = spawnSync("zig", ["version"], {
cwd: zigDirectory,
encoding: "utf8",
@@ -22,7 +22,7 @@ function hasFlag(name: string): boolean {
const libVariant = process.env.NATIVE_SPAN_FEED_LIB
const libBase = libVariant === "bench" ? "native_span_feed_bench" : (libVariant ?? "native_span_feed")
const libPath = new URL(`../zig/zig-out/lib/lib${libBase}.${suffix}`, import.meta.url).pathname
const libPath = new URL(`../../../native/zig-out/lib/lib${libBase}.${suffix}`, import.meta.url).pathname
setRenderLibPath(libPath)
@@ -8,11 +8,11 @@ The benchmark library (`libnative_span_feed_bench`) is built by the Zig bench-ff
with `ReleaseFast` by default. Override with `-Dbench-optimize=` if needed.
```bash
cd packages/core/src/zig
cd packages/native
zig build bench-ffi
```
This installs `zig-out/lib/libnative_span_feed_bench.*`, which
This installs `packages/native/zig-out/lib/libnative_span_feed_bench.*`, which
`src/benchmark/native-span-feed-benchmark.ts` loads by default.
You can also run `zig build bench` to build the bench runner and install the FFI bench library in one step.
@@ -44,7 +44,7 @@ const jsonPath = jsonArg ?? (hasFlag("json") ? jsonDefault : null)
const libVariant = process.env.NATIVE_SPAN_FEED_LIB
const libBase = libVariant === "bench" ? "native_span_feed_bench" : (libVariant ?? "native_span_feed")
const libPath = new URL(`../zig/zig-out/lib/lib${libBase}.${suffix}`, import.meta.url).pathname
const libPath = new URL(`../../../native/zig-out/lib/lib${libBase}.${suffix}`, import.meta.url).pathname
setRenderLibPath(libPath)
@@ -5,7 +5,7 @@ import { resolveRenderLib } from "../zig.js"
const png = Uint8Array.from(
Buffer.from(
readFileSync(new URL("../zig/tests/fixtures/display-p3.png.base64", import.meta.url), "utf8").trim(),
readFileSync(new URL("../../../native/src/tests/fixtures/display-p3.png.base64", import.meta.url), "utf8").trim(),
"base64",
),
)
@@ -16,7 +16,10 @@ const FIXTURES = new URL("./fixtures/images/", import.meta.url)
async function readBase64Fixture(name: string): Promise<Uint8Array> {
return Uint8Array.from(
Buffer.from((await readFile(new URL(`../zig/tests/fixtures/${name}`, import.meta.url), "utf8")).trim(), "base64"),
Buffer.from(
(await readFile(new URL(`../../../native/src/tests/fixtures/${name}`, import.meta.url), "utf8")).trim(),
"base64",
),
)
}
+4 -1
View File
@@ -29,7 +29,10 @@ const FIXTURES = new URL("./fixtures/images/", import.meta.url)
async function readBase64Fixture(name: string): Promise<Uint8Array> {
return Uint8Array.from(
Buffer.from((await readFile(new URL(`../zig/tests/fixtures/${name}`, import.meta.url), "utf8")).trim(), "base64"),
Buffer.from(
(await readFile(new URL(`../../../native/src/tests/fixtures/${name}`, import.meta.url), "utf8")).trim(),
"base64",
),
)
}
+7
View File
@@ -0,0 +1,7 @@
.zig-cache
zig-out
lib
zig-pkg
examples/*/.zig-cache
examples/*/zig-out
examples/*/zig-pkg
@@ -32,7 +32,7 @@ const SUPPORTED_TARGETS = [_]SupportedTarget{
const DEFAULT_MACOS_SDK_PATH = "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk";
const LIB_NAME = "opentui";
const ROOT_SOURCE_FILE = "lib.zig";
const ROOT_SOURCE_FILE = "src/lib.zig";
const GHOSTTY_VT_VERSION = "0.1.0-dev+b988efcf";
const YOGA_CXX_FLAGS = [_][]const u8{
@@ -185,10 +185,10 @@ fn addMiniaudioShim(
else => &.{"-std=c99"},
};
module.addIncludePath(b.path("."));
module.addIncludePath(b.path("src"));
module.link_libc = true;
module.addCSourceFile(.{
.file = b.path("miniaudio_shim.c"),
.file = b.path("src/miniaudio_shim.c"),
.flags = c_flags,
});
}
@@ -207,13 +207,13 @@ fn addImageShim(b: *std.Build, module: *std.Build.Module, target: std.Build.Reso
};
module.addCSourceFile(.{
.file = b.path("image-shim.c"),
.file = b.path("src/image-shim.c"),
.flags = flags,
});
module.addIncludePath(b.path("vendor/lcms2/include"));
module.addIncludePath(b.path("vendor/lcms2/src"));
module.addIncludePath(b.path("src/vendor/lcms2/include"));
module.addIncludePath(b.path("src/vendor/lcms2/src"));
module.addCSourceFiles(.{
.root = b.path("vendor/lcms2"),
.root = b.path("src/vendor/lcms2"),
.files = &LCMS2_SOURCES,
// image.zig serializes every LittleCMS operation with icc_cache_mutex.
.flags = flags,
@@ -230,7 +230,7 @@ fn addImageShim(b: *std.Build, module: *std.Build.Module, target: std.Build.Reso
else => &.{ "-std=c99", "-ffp-contract=off", "-fvisibility=hidden", "-fno-sanitize=bounds" },
};
module.addCSourceFile(.{
.file = b.path("image-resize-shim.c"),
.file = b.path("src/image-resize-shim.c"),
.flags = resize_flags,
});
@@ -242,13 +242,13 @@ fn addImageShim(b: *std.Build, module: *std.Build.Module, target: std.Build.Reso
appendCFlags(b, webp_flags, &.{ "-DWEBP_HAVE_SSE2", "-DWEBP_HAVE_SSE41", "-DWEBP_HAVE_AVX2" })
else
webp_flags;
module.addIncludePath(b.path("vendor/libwebp"));
module.addIncludePath(b.path("src/vendor/libwebp"));
module.addCSourceFile(.{
.file = b.path("image-webp-config.c"),
.file = b.path("src/image-webp-config.c"),
.flags = webp_dispatch_flags,
});
module.addCSourceFiles(.{
.root = b.path("vendor/libwebp"),
.root = b.path("src/vendor/libwebp"),
.files = &.{
"src/dec/alpha_dec.c",
"src/dec/buffer_dec.c",
@@ -286,7 +286,7 @@ fn addImageShim(b: *std.Build, module: *std.Build.Module, target: std.Build.Reso
switch (target.result.cpu.arch) {
.x86_64 => {
module.addCSourceFiles(.{
.root = b.path("vendor/libwebp"),
.root = b.path("src/vendor/libwebp"),
.files = &.{
"src/dsp/alpha_processing_sse2.c",
"src/dsp/dec_sse2.c",
@@ -299,16 +299,16 @@ fn addImageShim(b: *std.Build, module: *std.Build.Module, target: std.Build.Reso
.flags = webp_flags,
});
module.addCSourceFile(.{
.file = b.path("image-webp-sse41.c"),
.file = b.path("src/image-webp-sse41.c"),
.flags = webp_flags,
});
module.addCSourceFile(.{
.file = b.path("image-webp-avx2.c"),
.file = b.path("src/image-webp-avx2.c"),
.flags = webp_flags,
});
},
.aarch64 => module.addCSourceFiles(.{
.root = b.path("vendor/libwebp"),
.root = b.path("src/vendor/libwebp"),
.files = &.{
"src/dsp/alpha_processing_neon.c",
"src/dsp/dec_neon.c",
@@ -337,7 +337,7 @@ fn addMacOSSDKSearchPaths(b: *std.Build, module: *std.Build.Module, sdk_path: []
fn addMacOSClipboardDependencies(b: *std.Build, module: *std.Build.Module, sdk_path: []const u8) void {
module.addCSourceFile(.{
.file = b.path("clipboard/macos-shim.m"),
.file = b.path("src/clipboard/macos-shim.m"),
.flags = &.{ "-fobjc-arc", "-fobjc-arc-exceptions", "-isysroot", sdk_path },
});
module.linkFramework("AppKit", .{});
@@ -406,11 +406,11 @@ fn addTranslatedCImports(
target: std.Build.ResolvedTarget,
) void {
const miniaudio_translate = b.addTranslateC(.{
.root_source_file = b.path("vendor/miniaudio/miniaudio.h"),
.root_source_file = b.path("src/vendor/miniaudio/miniaudio.h"),
.target = target,
.optimize = optimize,
});
miniaudio_translate.addIncludePath(b.path("."));
miniaudio_translate.addIncludePath(b.path("src"));
module.addImport("miniaudio", miniaudio_translate.createModule());
const yoga_dep = b.dependency("yoga", .{});
@@ -509,13 +509,23 @@ pub fn build(b: *std.Build) void {
const optimize = b.standardOptimizeOption(.{});
const bench_optimize = b.option(std.builtin.OptimizeMode, "bench-optimize", "Optimize mode for benchmarks") orelse .ReleaseFast;
const debug_use_llvm = b.option(bool, "debug-llvm", "Use LLVM backend for debug/test artifacts");
const target_option = b.option([]const u8, "target", "Build for specific target (e.g., 'x86_64-linux-gnu.2.17').");
const target_option = b.option([]const u8, "library-target", "Build shared library for a specific target (e.g., 'x86_64-linux-gnu.2.17').");
const build_all = b.option(bool, "all", "Build for all supported targets") orelse false;
const gpa_safe_stats = b.option(bool, "gpa-safe-stats", "Enable GPA safety checks for trustworthy allocator stats") orelse false;
const macos_sdk_path = resolveMacOSSDKPath(b);
const build_options = b.addOptions();
build_options.addOption(bool, "gpa_safe_stats", gpa_safe_stats);
const module_target = b.standardTargetOptions(.{});
const opentui_module = b.addModule("opentui", .{
.root_source_file = b.path("src/opentui.zig"),
.target = module_target,
.optimize = optimize,
});
applyDependencies(b, opentui_module, optimize, module_target, build_options);
addNativeAudioDependencies(b, opentui_module, module_target, macos_sdk_path);
addYogaDependencies(b, opentui_module);
if (target_option) |target_str| {
// Build single target
buildSingleTarget(b, target_str, optimize, build_options, macos_sdk_path) catch |err| {
@@ -540,7 +550,7 @@ pub fn build(b: *std.Build) void {
const test_step = b.step("test", "Run unit tests");
const native_target = nativeExecutableTarget(b);
const test_mod = b.createModule(.{
.root_source_file = b.path("test.zig"),
.root_source_file = b.path("src/test.zig"),
.target = native_target,
.optimize = .Debug,
});
@@ -564,7 +574,7 @@ pub fn build(b: *std.Build) void {
// Bench step (native only)
const bench_step = b.step("bench", "Run benchmarks");
const bench_mod = b.createModule(.{
.root_source_file = b.path("bench.zig"),
.root_source_file = b.path("src/bench.zig"),
.target = native_target,
.optimize = bench_optimize,
});
@@ -585,7 +595,7 @@ pub fn build(b: *std.Build) void {
const bench_ffi_step = b.step("bench-ffi", "Build NativeSpanFeed benchmark library");
const bench_ffi_target = b.resolveTargetQuery(.{});
const bench_ffi_mod = b.createModule(.{
.root_source_file = b.path("native-span-feed-bench-lib.zig"),
.root_source_file = b.path("src/native-span-feed-bench-lib.zig"),
.target = bench_ffi_target,
.optimize = bench_optimize,
});
@@ -608,7 +618,7 @@ pub fn build(b: *std.Build) void {
// Debug step (native only)
const debug_step = b.step("debug", "Run debug executable");
const debug_mod = b.createModule(.{
.root_source_file = b.path("debug-view.zig"),
.root_source_file = b.path("src/debug-view.zig"),
.target = native_target,
.optimize = .Debug,
});
@@ -19,6 +19,8 @@
},
},
.paths = .{
".",
"build.zig",
"build.zig.zon",
"src",
},
}
+34
View File
@@ -0,0 +1,34 @@
const std = @import("std");
const builtin = @import("builtin");
pub fn build(b: *std.Build) void {
var default_target = b.graph.host.query;
if (builtin.os.tag == .linux) {
default_target.abi = .musl;
default_target.glibc_version = null;
}
const target = b.standardTargetOptions(.{ .default_target = default_target });
const optimize = b.standardOptimizeOption(.{});
const opentui = b.dependency("opentui", .{
.target = target,
.optimize = optimize,
});
const module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
module.addImport("opentui", opentui.module("opentui"));
const executable = b.addExecutable(.{
.name = "opentui-hello",
.root_module = module,
});
b.installArtifact(executable);
const run = b.addRunArtifact(executable);
run.step.dependOn(b.getInstallStep());
const run_step = b.step("run", "Render two frames without JavaScript");
run_step.dependOn(&run.step);
}
@@ -0,0 +1,16 @@
.{
.name = .opentui_hello,
.version = "0.0.0",
.fingerprint = 0xb62b2906a6ba79de,
.minimum_zig_version = "0.16.0",
.dependencies = .{
.opentui = .{
.path = "../..",
},
},
.paths = .{
"build.zig",
"build.zig.zon",
"src",
},
}
@@ -0,0 +1,92 @@
const std = @import("std");
const opentui = @import("opentui");
var io_threaded: std.Io.Threaded = .init_single_threaded;
pub const io = io_threaded.io();
const MemorySink = struct {
const capacity = 64 * 1024;
bytes: [capacity]u8 = undefined,
length: usize = 0,
fn output(self: *MemorySink) opentui.BufferedOutput {
return .{ .ctx = self, .write_fn = write };
}
fn write(context: *anyopaque, bytes: []const u8) void {
const self: *MemorySink = @ptrCast(@alignCast(context));
std.debug.assert(bytes.len <= capacity - self.length);
@memcpy(self.bytes[self.length..][0..bytes.len], bytes);
self.length += bytes.len;
}
fn slice(self: *const MemorySink) []const u8 {
return self.bytes[0..self.length];
}
fn clear(self: *MemorySink) void {
self.length = 0;
}
};
pub fn main() !void {
var debug_allocator: std.heap.DebugAllocator(.{}) = .init;
defer std.debug.assert(debug_allocator.deinit() == .ok);
const allocator = debug_allocator.allocator();
var graphemes = opentui.GraphemePool.init(allocator);
defer graphemes.deinit();
var links = opentui.LinkPool.init(allocator);
defer links.deinit();
var environment = std.process.Environ.Map.init(allocator);
defer environment.deinit();
var sink: MemorySink = .{};
if (opentui.CliRenderer.createWithOptions(allocator, 0, 1, &graphemes, .{
.output = .{ .buffered = sink.output() },
.clearOnShutdown = false,
.link_pool = &links,
.env_map = &environment,
})) |invalid_renderer| {
invalid_renderer.destroy();
return error.ExpectedInvalidDimensions;
} else |err| {
if (err != error.InvalidDimensions) return err;
}
const renderer = try opentui.CliRenderer.createWithOptions(allocator, 8, 1, &graphemes, .{
.output = .{ .buffered = sink.output() },
.clearOnShutdown = false,
.link_pool = &links,
.env_map = &environment,
});
defer renderer.destroy();
try renderer.getNextBuffer().drawText(
"OpenTUI",
0,
0,
opentui.rgbColor(255, 180, 40, 255),
opentui.rgbColor(10, 20, 30, 255),
opentui.TextAttributes.BOLD | opentui.TextAttributes.UNDERLINE,
);
if (renderer.render(true) != .rendered) return error.FirstFrameNotRendered;
const first_frame_length = sink.length;
if (std.mem.indexOf(u8, sink.slice(), "OpenTUI") == null) return error.MissingFirstFrameText;
sink.clear();
try renderer.resize(12, 2);
try renderer.getNextBuffer().drawText(
"resized",
1,
1,
opentui.rgbColor(80, 220, 255, 255),
null,
opentui.TextAttributes.ITALIC,
);
if (renderer.render(true) != .rendered) return error.SecondFrameNotRendered;
if (std.mem.indexOf(u8, sink.slice(), "resized") == null) return error.MissingSecondFrameText;
std.debug.print("rendered {d} and {d} bytes\n", .{ first_frame_length, sink.slice().len });
}
+13
View File
@@ -0,0 +1,13 @@
{
"name": "@opentui/native",
"version": "0.5.4",
"private": true,
"scripts": {
"build": "zig build -Doptimize=ReleaseFast",
"build:dev": "zig build -Doptimize=Debug",
"test": "sh scripts/test.sh",
"bench": "zig build bench -Dbench-optimize=ReleaseFast --",
"bench:ffi": "zig build bench-ffi",
"clean": "bun ../../scripts/clean.ts --native"
}
}
+5
View File
@@ -0,0 +1,5 @@
#!/bin/sh
set -eu
zig build test --summary all "$@"
(cd examples/hello && zig build run)
@@ -13,7 +13,7 @@ bun bench:native
# Include memory statistics
bun bench:native --mem
# Or from packages/core/src/zig directory:
# Or from packages/native:
zig build bench -Doptimize=ReleaseFast
zig build bench -Doptimize=ReleaseFast -- --mem
```
@@ -10,10 +10,10 @@ const EncodedScenario = struct {
};
const encoded_scenarios = [_]EncodedScenario{
.{ .label = "PNG", .path = "../../../examples/src/assets/image-demo.png" },
.{ .label = "JPEG", .path = "../../../examples/src/assets/dragon.jpg" },
.{ .label = "WebP", .path = "../../../examples/src/assets/image-demo.webp" },
.{ .label = "GIF", .path = "../../../examples/src/assets/image-demo.gif" },
.{ .label = "PNG", .path = "../examples/src/assets/image-demo.png" },
.{ .label = "JPEG", .path = "../examples/src/assets/dragon.jpg" },
.{ .label = "WebP", .path = "../examples/src/assets/image-demo.webp" },
.{ .label = "GIF", .path = "../examples/src/assets/image-demo.gif" },
};
fn appendResult(
@@ -109,7 +109,7 @@ fn appendDragonGeometryBenchmarks(
var gpa: std.heap.DebugAllocator(.{}) = .init;
defer _ = gpa.deinit();
const work_allocator = gpa.allocator();
const encoded = try std.Io.Dir.cwd().readFileAlloc(io, "../../../examples/src/assets/image-demo.gif", work_allocator, .limited(2 * 1024 * 1024));
const encoded = try std.Io.Dir.cwd().readFileAlloc(io, "../examples/src/assets/image-demo.gif", work_allocator, .limited(2 * 1024 * 1024));
defer work_allocator.free(encoded);
const decoded = try image.decode(work_allocator, encoded, .{});
defer decoded.deinit();
@@ -198,7 +198,7 @@ fn appendKittyBenchmarks(
var gpa: std.heap.DebugAllocator(.{}) = .init;
defer _ = gpa.deinit();
const work_allocator = gpa.allocator();
const encoded = try std.Io.Dir.cwd().readFileAlloc(io, "../../../examples/src/assets/image-demo.gif", work_allocator, .limited(2 * 1024 * 1024));
const encoded = try std.Io.Dir.cwd().readFileAlloc(io, "../examples/src/assets/image-demo.gif", work_allocator, .limited(2 * 1024 * 1024));
defer work_allocator.free(encoded);
const decoded = try image.decode(work_allocator, encoded, .{});
defer decoded.deinit();
@@ -345,7 +345,7 @@ fn appendKittyPngBenchmarks(
var gpa: std.heap.DebugAllocator(.{}) = .init;
defer _ = gpa.deinit();
const work_allocator = gpa.allocator();
const encoded = try std.Io.Dir.cwd().readFileAlloc(io, "../../../examples/src/assets/image-demo.png", work_allocator, .limited(2 * 1024 * 1024));
const encoded = try std.Io.Dir.cwd().readFileAlloc(io, "../examples/src/assets/image-demo.png", work_allocator, .limited(2 * 1024 * 1024));
defer work_allocator.free(encoded);
const decoded = try image.decode(work_allocator, encoded, .{});
defer decoded.deinit();
@@ -410,7 +410,7 @@ fn appendImageSwitchBenchmarks(
var gpa: std.heap.DebugAllocator(.{}) = .init;
defer _ = gpa.deinit();
const work_allocator = gpa.allocator();
const encoded = try std.Io.Dir.cwd().readFileAlloc(io, "../../../examples/src/assets/image-demo.gif", work_allocator, .limited(2 * 1024 * 1024));
const encoded = try std.Io.Dir.cwd().readFileAlloc(io, "../examples/src/assets/image-demo.gif", work_allocator, .limited(2 * 1024 * 1024));
defer work_allocator.free(encoded);
const decoded = try image.decode(work_allocator, encoded, .{});
defer decoded.deinit();

Some files were not shown because too many files have changed in this diff Show More