-
Notifications
You must be signed in to change notification settings - Fork 305
Add StatelessTerminal renderable. Render ANSI as opentui styled text #440
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
remorses
wants to merge
56
commits into
anomalyco:main
Choose a base branch
from
remorses:ghostty-opentui-2
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
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 be69a9b
fix(ci): update zig to 0.15.2 and skip macOS cross-compilation from L…
remorses 1fe9c14
fix: add macOS SDK for cross-compilation from Linux
remorses 59b5391
fix: add explicit refs to git URLs in build.zig.zon
remorses 7e8a205
fix: correct hash for macos_sdk dependency
remorses 08a4dad
fix: use raw macOS SDK tarball instead of outdated zig package
remorses 920c5d4
fix(ci): only build lib, not native binaries for all platforms
remorses d7e55c5
fix(ci): use macos-latest runners for cross-compilation
remorses 7492abd
fix: build only native platform by default, add --all for all platforms
remorses bac4cd3
fix: remove reference to non-existent ghostty-terminal.zig test file
remorses 82924dd
fix: update zig tests for 0.15 API changes
remorses b37dc0b
ci: retry
remorses 36ba112
fix: use arena allocator for ZON parser to avoid memory leaks in tests
remorses ed20f79
ci: use macos-large runner for faster builds
remorses 143c409
ci: revert to macos-latest, skip native tests (hang issue)
remorses 401c5cc
ci: trigger rebuild (transient network error)
remorses aff2fce
feat: add VTerm terminal rendering functions and renderables
remorses 8e27f53
feat: register Terminal renderables in solid, react, and vue
remorses 7f9ad9f
style: fix prettier formatting
remorses 05da396
fix read after free
remorses c166d28
use gpa for strings. use a arena for each persistent terminal
remorses b244bf7
use input buffers, nicer allocations, no longer leak
remorses c64388d
remove unnecessary std options
remorses 7ff1e61
disable ghostty logging
remorses 2a5c85d
add support for forwarding events in Terminal
remorses 800a7ed
do not allow changing streams
remorses 3a29ff4
show cursor in Terminal
remorses 5566465
cleanup demo
remorses b0b3400
fix: prettier formatting
remorses c1fb8bc
ci: retry
remorses 535b7b3
Merge remote-tracking branch 'upstream/main' into ghostty-opentui-2
remorses bb10ec8
Merge upstream/main into ghostty-opentui-2
remorses 0c4fa21
Update ghostty to latest, switch CI to ubuntu, fix type errors
remorses 77bfd69
Remove unused ts-expect-error directive
remorses 3e63499
Increase perf test threshold for CI variance
remorses db78f25
Switch remaining CI workflows to ubuntu
remorses 2a1c967
Use ghostty fork with musl PIC fix, remove -gnu suffix
remorses 6f1eb53
Add both glibc and musl linux targets
remorses 4176490
Add musl linux targets and runtime detection
remorses 933d59b
Use process.report for musl detection instead of filesystem check
remorses c70527d
Use fast filesystem check for musl detection
remorses ed1a57b
Fix formatting
remorses 8a5d5b7
ci: cross-compile all platforms and test on matrix of OSes
remorses ed4340f
ci: install gcc-multilib for cross-compilation
remorses f764fbd
ci: use macos runner for cross-compilation
remorses b22b0ba
ci: temporarily disable failing platform test matrices
remorses 3a01c02
switch ghostty dependency to official repo after PR merge
remorses b57378d
fix ghostty dependency hash
remorses 2c8d9f1
retry ci
remorses 0d9e750
Merge branch 'main' into ghostty-opentui-2
remorses 83f6878
use macos for cross building
remorses 8fe513e
add missing bun-pty package
remorses 3f67ebe
Merge branch 'ghostty-opentui-2' of https://github.com/remorses/opent…
remorses d6a5c5f
remove stateful TerminalRenderable, keep only StatelessTerminalRender…
remorses baf73bb
address PR review: threadlocal arena, better error message
remorses e0983dc
Merge remote-tracking branch 'upstream/main' into ghostty-opentui-2
remorses File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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" }, | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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" }, | ||
| ] | ||
|
|
@@ -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}` | ||
| } | ||
|
|
@@ -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 | ||
| } | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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() | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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