Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
4a41113
upgrade to zig 0.15.2 with ghostty-vt dependency
remorses Dec 23, 2025
be69a9b
fix(ci): update zig to 0.15.2 and skip macOS cross-compilation from L…
remorses Dec 23, 2025
1fe9c14
fix: add macOS SDK for cross-compilation from Linux
remorses Dec 23, 2025
59b5391
fix: add explicit refs to git URLs in build.zig.zon
remorses Dec 23, 2025
7e8a205
fix: correct hash for macos_sdk dependency
remorses Dec 23, 2025
08a4dad
fix: use raw macOS SDK tarball instead of outdated zig package
remorses Dec 23, 2025
920c5d4
fix(ci): only build lib, not native binaries for all platforms
remorses Dec 23, 2025
d7e55c5
fix(ci): use macos-latest runners for cross-compilation
remorses Dec 23, 2025
7492abd
fix: build only native platform by default, add --all for all platforms
remorses Dec 23, 2025
bac4cd3
fix: remove reference to non-existent ghostty-terminal.zig test file
remorses Dec 23, 2025
82924dd
fix: update zig tests for 0.15 API changes
remorses Dec 23, 2025
b37dc0b
ci: retry
remorses Dec 23, 2025
36ba112
fix: use arena allocator for ZON parser to avoid memory leaks in tests
remorses Dec 23, 2025
ed20f79
ci: use macos-large runner for faster builds
remorses Dec 23, 2025
143c409
ci: revert to macos-latest, skip native tests (hang issue)
remorses Dec 23, 2025
401c5cc
ci: trigger rebuild (transient network error)
remorses Dec 23, 2025
aff2fce
feat: add VTerm terminal rendering functions and renderables
remorses Dec 23, 2025
8e27f53
feat: register Terminal renderables in solid, react, and vue
remorses Dec 23, 2025
7f9ad9f
style: fix prettier formatting
remorses Dec 23, 2025
05da396
fix read after free
remorses Dec 24, 2025
c166d28
use gpa for strings. use a arena for each persistent terminal
remorses Dec 24, 2025
b244bf7
use input buffers, nicer allocations, no longer leak
remorses Dec 24, 2025
c64388d
remove unnecessary std options
remorses Dec 24, 2025
7ff1e61
disable ghostty logging
remorses Dec 24, 2025
2a5c85d
add support for forwarding events in Terminal
remorses Dec 24, 2025
800a7ed
do not allow changing streams
remorses Dec 24, 2025
3a29ff4
show cursor in Terminal
remorses Dec 24, 2025
5566465
cleanup demo
remorses Dec 24, 2025
b0b3400
fix: prettier formatting
remorses Dec 24, 2025
c1fb8bc
ci: retry
remorses Dec 24, 2025
535b7b3
Merge remote-tracking branch 'upstream/main' into ghostty-opentui-2
remorses Jan 6, 2026
bb10ec8
Merge upstream/main into ghostty-opentui-2
remorses Jan 6, 2026
0c4fa21
Update ghostty to latest, switch CI to ubuntu, fix type errors
remorses Jan 6, 2026
77bfd69
Remove unused ts-expect-error directive
remorses Jan 6, 2026
3e63499
Increase perf test threshold for CI variance
remorses Jan 6, 2026
db78f25
Switch remaining CI workflows to ubuntu
remorses Jan 6, 2026
2a1c967
Use ghostty fork with musl PIC fix, remove -gnu suffix
remorses Jan 7, 2026
6f1eb53
Add both glibc and musl linux targets
remorses Jan 7, 2026
4176490
Add musl linux targets and runtime detection
remorses Jan 7, 2026
933d59b
Use process.report for musl detection instead of filesystem check
remorses Jan 7, 2026
c70527d
Use fast filesystem check for musl detection
remorses Jan 7, 2026
ed1a57b
Fix formatting
remorses Jan 7, 2026
8a5d5b7
ci: cross-compile all platforms and test on matrix of OSes
remorses Jan 7, 2026
ed4340f
ci: install gcc-multilib for cross-compilation
remorses Jan 7, 2026
f764fbd
ci: use macos runner for cross-compilation
remorses Jan 7, 2026
b22b0ba
ci: temporarily disable failing platform test matrices
remorses Jan 7, 2026
3a01c02
switch ghostty dependency to official repo after PR merge
remorses Jan 7, 2026
b57378d
fix ghostty dependency hash
remorses Jan 7, 2026
2c8d9f1
retry ci
remorses Jan 7, 2026
0d9e750
Merge branch 'main' into ghostty-opentui-2
remorses Jan 8, 2026
83f6878
use macos for cross building
remorses Jan 8, 2026
8fe513e
add missing bun-pty package
remorses Jan 8, 2026
3f67ebe
Merge branch 'ghostty-opentui-2' of https://github.com/remorses/opent…
remorses Jan 8, 2026
d6a5c5f
remove stateful TerminalRenderable, keep only StatelessTerminalRender…
remorses Jan 13, 2026
baf73bb
address PR review: threadlocal arena, better error message
remorses Jan 13, 2026
e0983dc
Merge remote-tracking branch 'upstream/main' into ghostty-opentui-2
remorses Jan 13, 2026
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
57 changes: 52 additions & 5 deletions .github/workflows/build-core.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ on:
branches: [main]

jobs:
build:
name: Core - Build and Test
build-native:
name: Build Native (All Platforms)
runs-on: macos-latest
steps:
- name: Checkout code
Expand All @@ -26,12 +26,59 @@ jobs:
- name: Install dependencies
run: bun install

- name: Build
- name: Build native for all platforms
run: |
cd packages/core
bun run build
bun run build:native --all

- name: Upload native artifacts
uses: actions/upload-artifact@v4
with:
name: native-all
path: packages/core/node_modules/@opentui/
retention-days: 1

test-ts:
name: Test (${{ matrix.name }})
needs: build-native
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
name: linux-x64
Copy link
Contributor Author

@remorses remorses Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added more OS machines in SO that would run the tests. Currently tests are failing for other OS other than linux but the binaries build correctly. I have another branch that fixes the flaky tests in other OSes

# TODO: fix platform-specific test failures before re-enabling
# - os: ubuntu-latest
# name: linux-musl-x64
# container: oven/bun:alpine
# - os: macos-latest
# name: darwin-arm64
# - os: macos-13
# name: darwin-x64
# - os: windows-latest
# name: win32-x64
runs-on: ${{ matrix.os }}
container: ${{ matrix.container || '' }}
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup Bun
if: ${{ !matrix.container }}
uses: oven-sh/setup-bun@v2
with:
bun-version: latest

- name: Install dependencies
run: bun install

- name: Download native artifacts
uses: actions/download-artifact@v4
with:
name: native-all
path: packages/core/node_modules/@opentui/

- name: Run tests
run: |
cd packages/core
bun run test
bun run test:js
2 changes: 1 addition & 1 deletion .github/workflows/build-examples.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ on:
jobs:
build-examples:
name: Build Example Executables
runs-on: macos-latest
runs-on: ubuntu-latest

steps:
- name: Checkout code
Expand Down
15 changes: 11 additions & 4 deletions packages/core/scripts/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,15 @@ const args = process.argv.slice(2)
const buildLib = args.find((arg) => arg === "--lib")
const buildNative = args.find((arg) => arg === "--native")
const isDev = args.includes("--dev")
const buildAll = args.includes("--all") // Build for all platforms
const buildAll = args.includes("--all") // Build for all platforms (requires macOS or cross-compilation setup)

const variants: Variant[] = [
{ platform: "darwin", arch: "x64" },
{ platform: "darwin", arch: "arm64" },
{ platform: "linux", arch: "x64" },
{ platform: "linux", arch: "arm64" },
{ platform: "linux-musl", arch: "x64" },
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

musl not requires a separate target triplet because of Ghostty dependency compiling differently for gnu and musl

{ platform: "linux-musl", arch: "arm64" },
{ platform: "win32", arch: "x64" },
{ platform: "win32", arch: "arm64" },
]
Expand All @@ -57,7 +59,12 @@ if (!buildLib && !buildNative) {
}

const getZigTarget = (platform: string, arch: string): string => {
const platformMap: Record<string, string> = { darwin: "macos", win32: "windows", linux: "linux" }
const platformMap: Record<string, string> = {
darwin: "macos",
win32: "windows",
linux: "linux",
"linux-musl": "linux-musl",
}
const archMap: Record<string, string> = { x64: "x86_64", arm64: "aarch64" }
return `${archMap[arch] ?? arch}-${platformMap[platform] ?? platform}`
}
Expand Down Expand Up @@ -126,8 +133,8 @@ if (buildNative) {
}

if (copiedFiles === 0) {
// Skip platforms that weren't built
console.log(`Skipping ${platform}-${arch}: no libraries found`)
// Skip platforms that weren't built (e.g., macOS when cross-compiling from Linux)
console.log(`Skipping ${platform}-${arch}: no libraries found (cross-compilation may not be supported)`)
rmSync(nativeDir, { recursive: true, force: true })
continue
}
Expand Down
162 changes: 162 additions & 0 deletions packages/core/src/examples/terminal-simple-demo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import {
createCliRenderer,
StatelessTerminalRenderable,
BoxRenderable,
type CliRenderer,
type KeyEvent,
ScrollBoxRenderable,
} from "../index"
import { TextRenderable } from "../renderables/Text"
import { setupCommonDemoKeys } from "./lib/standalone-keys"

let renderer: CliRenderer | null = null
let terminalDisplay: StatelessTerminalRenderable | null = null
let scrollBox: ScrollBoxRenderable | null = null
let statusDisplay: TextRenderable | null = null

const SAMPLE_ANSI = `\x1b[1;32muser@hostname\x1b[0m:\x1b[1;34m~/projects/my-app\x1b[0m$ ls -la
total 128
drwxr-xr-x 12 user user 4096 Nov 26 10:30 \x1b[1;34m.\x1b[0m
drwxr-xr-x 5 user user 4096 Nov 25 14:22 \x1b[1;34m..\x1b[0m
-rw-r--r-- 1 user user 234 Nov 26 10:30 .gitignore
drwxr-xr-x 8 user user 4096 Nov 26 10:28 \x1b[1;34m.git\x1b[0m
-rw-r--r-- 1 user user 1842 Nov 26 09:15 package.json

\x1b[1;32muser@hostname\x1b[0m:\x1b[1;34m~/projects/my-app\x1b[0m$ git status
On branch \x1b[1;36mmain\x1b[0m
Changes to be committed:
\x1b[32mmodified: src/index.ts\x1b[0m
\x1b[32mnew file: src/utils.ts\x1b[0m

Changes not staged for commit:
\x1b[31mmodified: package.json\x1b[0m

\x1b[1;32muser@hostname\x1b[0m:\x1b[1;34m~/projects/my-app\x1b[0m$ npm run build
\x1b[1;33m[WARN]\x1b[0m Deprecation warning: 'fs.exists' is deprecated
\x1b[1;36m[INFO]\x1b[0m Compiling TypeScript files...
\x1b[1;32m[SUCCESS]\x1b[0m Build completed in 2.34s

\x1b[1;32muser@hostname\x1b[0m:\x1b[1;34m~/projects/my-app\x1b[0m$ echo "Style showcase:"
Style showcase:

\x1b[1mBold text\x1b[0m
\x1b[2mFaint/dim text\x1b[0m
\x1b[3mItalic text\x1b[0m
\x1b[4mUnderlined text\x1b[0m
\x1b[7mInverse/reverse text\x1b[0m
\x1b[9mStrikethrough text\x1b[0m

\x1b[31mRed\x1b[0m \x1b[32mGreen\x1b[0m \x1b[33mYellow\x1b[0m \x1b[34mBlue\x1b[0m \x1b[35mMagenta\x1b[0m \x1b[36mCyan\x1b[0m
\x1b[38;5;208mOrange (256 color)\x1b[0m
\x1b[38;2;255;105;180mHot Pink (RGB)\x1b[0m
`

let currentAnsi = SAMPLE_ANSI
let prefixCount = 0

export function run(rendererInstance: CliRenderer): void {
renderer = rendererInstance
renderer.setBackgroundColor("#0d1117")

const container = new BoxRenderable(renderer, {
id: "container",
flexDirection: "column",
flexGrow: 1,
})
renderer.root.add(container)

statusDisplay = new TextRenderable(renderer, {
id: "status",
content: "Press 'p' to add prefix | 't' scroll top | 'b' scroll bottom | 'q' to quit",
height: 1,
fg: "#8b949e",
padding: 1,
})
container.add(statusDisplay)

scrollBox = new ScrollBoxRenderable(renderer, {
id: "scroll-box",
flexGrow: 1,
padding: 1,
})
container.add(scrollBox)

terminalDisplay = new StatelessTerminalRenderable(renderer, {
id: "terminal",
ansi: currentAnsi,
cols: 120,
rows: 100,
trimEnd: true,
})
scrollBox.add(terminalDisplay)

rendererInstance.keyInput.on("keypress", handleKey)
}

function handleKey(key: KeyEvent): void {
if (key.name === "q" || key.name === "escape") {
process.exit(0)
}

if (key.name === "p" && terminalDisplay) {
prefixCount++
const prefix = `\x1b[1;35m[PREFIX ${prefixCount}]\x1b[0m\n`
currentAnsi = prefix + currentAnsi
terminalDisplay.ansi = currentAnsi
updateStatus()
}

if (key.name === "t" && scrollBox) {
scrollBox.scrollTo(0)
}

if (key.name === "b" && scrollBox && terminalDisplay) {
const lastLine = terminalDisplay.lineCount - 1
const scrollPos = terminalDisplay.getScrollPositionForLine(lastLine)
scrollBox.scrollTo(scrollPos)
}
}

function updateStatus(): void {
if (statusDisplay && terminalDisplay) {
statusDisplay.content = `Press 'p' to add prefix | 't' top | 'b' bottom | 'q' quit | Prefixes: ${prefixCount} | Lines: ${terminalDisplay.lineCount}`
}
}

export function destroy(rendererInstance: CliRenderer): void {
rendererInstance.keyInput.off("keypress", handleKey)

if (terminalDisplay) {
terminalDisplay.destroy()
terminalDisplay = null
}

if (scrollBox) {
scrollBox.destroy()
scrollBox = null
}

if (statusDisplay) {
statusDisplay.destroy()
statusDisplay = null
}

rendererInstance.root.remove("container")
renderer = null
}

if (import.meta.main) {
const inputFile = process.argv[2]
if (inputFile) {
const fs = await import("fs")
currentAnsi = fs.readFileSync(inputFile, "utf-8")
}

const renderer = await createCliRenderer({
exitOnCtrlC: true,
})

run(renderer)
setupCommonDemoKeys(renderer)
renderer.start()
}
1 change: 1 addition & 0 deletions packages/core/src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ export * from "./tree-sitter"
export * from "./data-paths"
export * from "./extmarks"
export * from "./terminal-palette"
export * from "./vterm-ffi"
80 changes: 80 additions & 0 deletions packages/core/src/lib/vterm-ffi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { StyledText } from "./styled-text"
import { RGBA } from "./RGBA"
import type { TextChunk } from "../text-buffer"
import { TextAttributes } from "../types"

const DEFAULT_FG = RGBA.fromHex("#d4d4d4")

export const VTermStyleFlags = {
BOLD: 1,
ITALIC: 2,
UNDERLINE: 4,
STRIKETHROUGH: 8,
INVERSE: 16,
FAINT: 32,
} as const

export interface VTermSpan {
text: string
fg: string | null
bg: string | null
flags: number
width: number
}

export interface VTermLine {
spans: VTermSpan[]
}

export interface VTermData {
cols: number
rows: number
cursor: [number, number]
offset: number
totalLines: number
lines: VTermLine[]
}

function convertSpanToChunk(span: VTermSpan): TextChunk {
const { text, fg, bg, flags } = span

let fgColor = fg ? RGBA.fromHex(fg) : DEFAULT_FG
let bgColor = bg ? RGBA.fromHex(bg) : undefined

if (flags & VTermStyleFlags.INVERSE) {
const temp = fgColor
fgColor = bgColor || DEFAULT_FG
bgColor = temp
}

let attributes = 0
if (flags & VTermStyleFlags.BOLD) attributes |= TextAttributes.BOLD
if (flags & VTermStyleFlags.ITALIC) attributes |= TextAttributes.ITALIC
if (flags & VTermStyleFlags.UNDERLINE) attributes |= TextAttributes.UNDERLINE
if (flags & VTermStyleFlags.STRIKETHROUGH) attributes |= TextAttributes.STRIKETHROUGH
if (flags & VTermStyleFlags.FAINT) attributes |= TextAttributes.DIM

return { __isChunk: true, text, fg: fgColor, bg: bgColor, attributes }
}

export function vtermDataToStyledText(data: VTermData): StyledText {
const chunks: TextChunk[] = []

for (let i = 0; i < data.lines.length; i++) {
const line = data.lines[i]

if (line.spans.length === 0) {
chunks.push({ __isChunk: true, text: " ", attributes: 0 })
} else {
for (const span of line.spans) {
chunks.push(convertSpanToChunk(span))
}
}

if (i < data.lines.length - 1) {
chunks.push({ __isChunk: true, text: "\n", attributes: 0 })
}
}

return new StyledText(chunks)
}
Loading
Loading