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 authored and GitHub committed 2026-08-20 11:34:03 +02:00
1 parent 2417a2c0b7
commit 6dec16a77c
301 files changed
+366 -114

No files matched your search

+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)
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -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();
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Loaded 100 of 301 files, more files were not shown because too many files have changed in this diff. Show more