Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 59 additions & 4 deletions .github/workflows/build-core.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ on:

jobs:
build:
name: Core - Build and Test
runs-on: ubuntu-latest
name: Build Core Library
runs-on: macos-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
Expand All @@ -31,7 +31,62 @@ jobs:
cd packages/core
bun run build

- name: Run tests
- name: Run native tests
run: |
cd packages/core
bun run test
bun run test:native

- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: build-artifacts
path: |
packages/core/dist
packages/core/node_modules/@opentui/core-*
retention-days: 1

test:
name: Test - ${{ matrix.os }} (${{ matrix.arch }})
needs: build
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- os: macos
arch: arm64
runner: macos-latest
- os: macos
arch: x64
runner: macos-13
- os: linux
arch: x64
runner: ubuntu-latest
- os: linux
arch: arm64
runner: ubuntu-24.04-arm
- os: windows
arch: x64
runner: windows-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest

- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: build-artifacts
path: packages/core

- name: Install dependencies
run: bun install

- name: Run TypeScript tests
run: |
cd packages/core
bun run test:js
4 changes: 3 additions & 1 deletion packages/core/src/lib/tree-sitter/cache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,9 @@ describe("TreeSitterClient Caching", () => {
})

test("should handle directory creation errors gracefully", async () => {
const invalidDataPath = "/invalid/path/that/cannot/be/created"
// Use a null byte in the path to ensure it is invalid on all platforms.
// This helps test error handling for directory creation in a cross-platform way.
const invalidDataPath = "/invalid\x00/path/with/null/byte"
const client = new TreeSitterClient({ dataPath: invalidDataPath })

await expect(client.initialize()).rejects.toThrow()
Expand Down
3 changes: 1 addition & 2 deletions packages/core/src/lib/tree-sitter/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,9 @@ import type {
SimpleHighlight,
} from "./types"
import { getParsers } from "./default-parsers"
import { resolve, isAbsolute } from "path"
import { resolve, isAbsolute, parse } from "path"
import { existsSync } from "fs"
import { registerEnvVar, env } from "../env"
import { parse } from "path"

registerEnvVar({
name: "OTUI_TREE_SITTER_WORKER_PATH",
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/renderables/Text.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ describe("TextRenderable Selection", () => {
})

await currentMouse.drag(text.x, text.y, text.x + 5, text.y)
// Add delay to ensure all drag events are processed on slow CI machines
await new Promise((resolve) => setTimeout(resolve, 50))
await renderOnce()

const selectedText = text.getSelectedText()
Expand All @@ -46,6 +48,8 @@ describe("TextRenderable Selection", () => {

// Select "Hello 🌍" (7 characters: H,e,l,l,o, ,🌍)
await currentMouse.drag(text.x, text.y, text.x + 7, text.y)
// Add delay to ensure all drag events are processed on slow CI machines
await new Promise((resolve) => setTimeout(resolve, 50))
await renderOnce()

const selectedText = text.getSelectedText()
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/testing/mock-keys.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ describe("mock-keys", () => {

expect(timestamps).toHaveLength(2)
expect(timestamps[1] - timestamps[0]).toBeGreaterThanOrEqual(8) // Allow some tolerance
expect(timestamps[1] - timestamps[0]).toBeLessThan(20)
expect(timestamps[1] - timestamps[0]).toBeLessThan(50) // Increased tolerance for CI/slower machines
})

test("pressKey with shift modifier", () => {
Expand Down
40 changes: 29 additions & 11 deletions packages/core/src/zig/renderer.zig
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,32 @@ pub const DebugOverlayCorner = enum {
bottomRight,
};

const StdoutWriter = union(enum) {
real: std.io.BufferedWriter(4096, std.fs.File.Writer),
null: void,

pub fn writer(self: *StdoutWriter) Writer {
return .{ .context = self };
}

pub fn flush(self: *StdoutWriter) !void {
switch (self.*) {
.real => |*w| try w.flush(),
.null => {},
}
}

const WriteError = std.fs.File.WriteError;
const Writer = std.io.Writer(*StdoutWriter, WriteError, write);

fn write(self: *StdoutWriter, data: []const u8) WriteError!usize {
switch (self.*) {
.real => |*w| return w.writer().write(data),
.null => return data.len,
}
}
};

pub const CliRenderer = struct {
width: u32,
height: u32,
Expand Down Expand Up @@ -80,7 +106,7 @@ pub const CliRenderer = struct {
lastRenderTime: i64,
allocator: Allocator,
renderThread: ?std.Thread = null,
stdoutWriter: std.io.BufferedWriter(4096, std.fs.File.Writer),
stdoutWriter: StdoutWriter,
debugOverlay: struct {
enabled: bool,
corner: DebugOverlayCorner,
Expand Down Expand Up @@ -141,17 +167,9 @@ pub const CliRenderer = struct {
const currentBuffer = try OptimizedBuffer.init(allocator, width, height, .{ .pool = pool, .width_method = .unicode, .id = "current buffer" }, graphemes_data, display_width);
const nextBuffer = try OptimizedBuffer.init(allocator, width, height, .{ .pool = pool, .width_method = .unicode, .id = "next buffer" }, graphemes_data, display_width);

const stdoutWriter = if (testing) blk: {
// In testing mode, use /dev/null to discard output
const devnull = std.fs.openFileAbsolute("/dev/null", .{ .mode = .write_only }) catch {
// Fallback to stdout if /dev/null can't be opened
logger.warn("Failed to open /dev/null, falling back to stdout\n", .{});
break :blk std.io.BufferedWriter(4096, std.fs.File.Writer){ .unbuffered_writer = std.io.getStdOut().writer() };
};
break :blk std.io.BufferedWriter(4096, std.fs.File.Writer){ .unbuffered_writer = devnull.writer() };
} else blk: {
const stdoutWriter: StdoutWriter = if (testing) .{ .null = {} } else blk: {
const stdout = std.io.getStdOut();
break :blk std.io.BufferedWriter(4096, std.fs.File.Writer){ .unbuffered_writer = stdout.writer() };
break :blk .{ .real = std.io.BufferedWriter(4096, std.fs.File.Writer){ .unbuffered_writer = stdout.writer() } };
};

// stat sample arrays
Expand Down
Loading