Files
opentui/packages/react/examples/diff.tsx
T
Simon Klee c1bb64ba19 native: move the render tree into Zig (#1479)
TypeScript used to own the renderable tree. Each frame walked it in
JavaScript, read layout back from Yoga, and made one FFI call per draw,
so frame cost grew with node and draw count. Native objects lived in
global registries and crossed FFI as raw pointers, which made renderer
teardown fragile.

A Context-owned Scene now holds the retained tree, its Yoga nodes, and
its paint state. Renderables stage property changes and flush them in
one call per frame. Zig lays out, culls, and paints the built-in
widgets, fills the hit grid, and hands the frame to Session for the
terminal diff. A frame without paint hooks completes in one step call.
Paint hooks run in one batch before painting and record into a byte
stream that native replays in paint order, so painting never pauses
for JavaScript and a hook cannot tear the frame.

Every native resource sits behind a generational handle, so a stale
handle fails with an error instead of touching freed memory, and
destroying a Context releases everything it owns.
2026-10-08 11:47:53 +02:00

319 lines
9.5 KiB
TypeScript

import { createCliRenderer, DiffRenderable, parseColor, RenderableEvents, SyntaxStyle } from "@opentui/core"
import { createRoot, useKeyboard, useRenderer } from "@opentui/react"
import { useState, useLayoutEffect, useRef } from "react"
interface DiffTheme {
name: string
backgroundColor: string
borderColor: string
addedBg: string
removedBg: string
contextBg: string
addedSignColor: string
removedSignColor: string
lineNumberFg: string
lineNumberBg: string
addedLineNumberBg: string
removedLineNumberBg: string
selectionBg: string
selectionFg: string
syntaxStyle: Parameters<typeof SyntaxStyle.fromStyles>[0]
}
const themes: DiffTheme[] = [
{
name: "GitHub Dark",
backgroundColor: "#0D1117",
borderColor: "#4ECDC4",
addedBg: "#1a4d1a",
removedBg: "#4d1a1a",
contextBg: "transparent",
addedSignColor: "#22c55e",
removedSignColor: "#ef4444",
lineNumberFg: "#6b7280",
lineNumberBg: "#161b22",
addedLineNumberBg: "#0d3a0d",
removedLineNumberBg: "#3a0d0d",
selectionBg: "#264F78",
selectionFg: "#FFFFFF",
syntaxStyle: {
keyword: { fg: parseColor("#FF7B72"), bold: true },
"keyword.import": { fg: parseColor("#FF7B72"), bold: true },
string: { fg: parseColor("#A5D6FF") },
comment: { fg: parseColor("#8B949E"), italic: true },
number: { fg: parseColor("#79C0FF") },
boolean: { fg: parseColor("#79C0FF") },
constant: { fg: parseColor("#79C0FF") },
function: { fg: parseColor("#D2A8FF") },
"function.call": { fg: parseColor("#D2A8FF") },
constructor: { fg: parseColor("#FFA657") },
type: { fg: parseColor("#FFA657") },
operator: { fg: parseColor("#FF7B72") },
variable: { fg: parseColor("#E6EDF3") },
property: { fg: parseColor("#79C0FF") },
bracket: { fg: parseColor("#F0F6FC") },
punctuation: { fg: parseColor("#F0F6FC") },
default: { fg: parseColor("#E6EDF3") },
},
},
{
name: "Monokai",
backgroundColor: "#272822",
borderColor: "#FD971F",
addedBg: "#2d4a2b",
removedBg: "#4a2b2b",
contextBg: "transparent",
addedSignColor: "#A6E22E",
removedSignColor: "#F92672",
lineNumberFg: "#75715E",
lineNumberBg: "#1e1f1c",
addedLineNumberBg: "#1e3a1e",
removedLineNumberBg: "#3a1e1e",
selectionBg: "#49483E",
selectionFg: "#F8F8F2",
syntaxStyle: {
keyword: { fg: parseColor("#F92672"), bold: true },
"keyword.import": { fg: parseColor("#F92672"), bold: true },
string: { fg: parseColor("#E6DB74") },
comment: { fg: parseColor("#75715E"), italic: true },
number: { fg: parseColor("#AE81FF") },
boolean: { fg: parseColor("#AE81FF") },
constant: { fg: parseColor("#AE81FF") },
function: { fg: parseColor("#A6E22E") },
"function.call": { fg: parseColor("#A6E22E") },
constructor: { fg: parseColor("#FD971F") },
type: { fg: parseColor("#66D9EF") },
operator: { fg: parseColor("#F92672") },
variable: { fg: parseColor("#F8F8F2") },
property: { fg: parseColor("#66D9EF") },
bracket: { fg: parseColor("#F8F8F2") },
punctuation: { fg: parseColor("#F8F8F2") },
default: { fg: parseColor("#F8F8F2") },
},
},
{
name: "Dracula",
backgroundColor: "#282A36",
borderColor: "#BD93F9",
addedBg: "#2d4737",
removedBg: "#4d2d37",
contextBg: "transparent",
addedSignColor: "#50FA7B",
removedSignColor: "#FF5555",
lineNumberFg: "#6272A4",
lineNumberBg: "#21222C",
addedLineNumberBg: "#1f3626",
removedLineNumberBg: "#3a2328",
selectionBg: "#44475A",
selectionFg: "#F8F8F2",
syntaxStyle: {
keyword: { fg: parseColor("#FF79C6"), bold: true },
"keyword.import": { fg: parseColor("#FF79C6"), bold: true },
string: { fg: parseColor("#F1FA8C") },
comment: { fg: parseColor("#6272A4"), italic: true },
number: { fg: parseColor("#BD93F9") },
boolean: { fg: parseColor("#BD93F9") },
constant: { fg: parseColor("#BD93F9") },
function: { fg: parseColor("#50FA7B") },
"function.call": { fg: parseColor("#50FA7B") },
constructor: { fg: parseColor("#FFB86C") },
type: { fg: parseColor("#8BE9FD") },
operator: { fg: parseColor("#FF79C6") },
variable: { fg: parseColor("#F8F8F2") },
property: { fg: parseColor("#8BE9FD") },
bracket: { fg: parseColor("#F8F8F2") },
punctuation: { fg: parseColor("#F8F8F2") },
default: { fg: parseColor("#F8F8F2") },
},
},
]
const exampleDiff = `--- a/calculator.ts
+++ b/calculator.ts
@@ -1,13 +1,20 @@
class Calculator {
add(a: number, b: number): number {
return a + b;
}
- subtract(a: number, b: number): number {
- return a - b;
+ subtract(a: number, b: number, c: number = 0): number {
+ return a - b - c;
}
multiply(a: number, b: number): number {
return a * b;
}
+
+ divide(a: number, b: number): number {
+ if (b === 0) {
+ throw new Error("Division by zero");
+ }
+ return a / b;
+ }
}`
const HelpModal = ({ theme, visible }: { theme: DiffTheme; visible: boolean }) => {
if (!visible) return null
return (
<box
style={{
position: "absolute",
left: "50%",
top: "50%",
width: 60,
height: 14,
marginLeft: -30,
marginTop: -7,
border: true,
borderStyle: "double",
borderColor: theme.borderColor,
backgroundColor: theme.backgroundColor,
padding: 2,
zIndex: 100,
}}
title="Keybindings"
titleAlignment="center"
>
<text
content={`View Controls:
V : Toggle view mode (Unified/Split)
L : Toggle line numbers
W : Toggle wrap mode (None/Word)
Theme & Display:
T : Cycle through themes
Other:
? : Toggle this help screen
Ctrl+C : Exit`}
style={{ fg: "#E6EDF3" }}
/>
</box>
)
}
export function App() {
const renderer = useRenderer()
const diffRef = useRef<DiffRenderable | null>(null)
const ownedStyle = useRef<SyntaxStyle | null>(null)
const [syntaxStyle, setSyntaxStyle] = useState<SyntaxStyle | null>(null)
const [themeIndex, setThemeIndex] = useState(0)
const [view, setView] = useState<"unified" | "split">("unified")
const [showLineNumbers, setShowLineNumbers] = useState(true)
const [wrapMode, setWrapMode] = useState<"none" | "word">("none")
const [showHelp, setShowHelp] = useState(false)
const theme = themes[themeIndex]
useLayoutEffect(() => {
const style = SyntaxStyle.fromStyles(theme.syntaxStyle, renderer.nativeScene!)
const previous = ownedStyle.current
ownedStyle.current = style
try {
const diff = diffRef.current
if (diff && !diff.isDestroyed) diff.syntaxStyle = style
setSyntaxStyle(style)
} finally {
previous?.destroy()
}
}, [renderer, theme])
useLayoutEffect(() => {
return () => {
const style = ownedStyle.current
ownedStyle.current = null
if (!style) return
const diff = diffRef.current
if (diff && !diff.isDestroyed) {
// Diff destroys its detached panes after emitting its own destroy event.
diff.prependOnceListener(RenderableEvents.DESTROYED, () => queueMicrotask(() => style.destroy()))
} else {
style.destroy()
}
}
}, [renderer])
useKeyboard((key) => {
if (key.raw === "?") {
setShowHelp((prev) => !prev)
return
}
if (showHelp) return
if (key.name === "v" && !key.ctrl && !key.meta) {
setView((prev) => (prev === "unified" ? "split" : "unified"))
} else if (key.name === "l" && !key.ctrl && !key.meta) {
setShowLineNumbers((prev) => !prev)
} else if (key.name === "w" && !key.ctrl && !key.meta) {
setWrapMode((prev) => (prev === "none" ? "word" : "none"))
} else if (key.name === "t" && !key.ctrl && !key.meta) {
setThemeIndex((prev) => (prev + 1) % themes.length)
}
})
if (!syntaxStyle) return null
return (
<box
style={{
padding: 1,
backgroundColor: theme.backgroundColor,
zIndex: 10,
}}
>
<box
title={`Diff Demo - ${theme.name}`}
titleAlignment="center"
style={{
height: 3,
border: true,
borderStyle: "double",
borderColor: theme.borderColor,
backgroundColor: theme.backgroundColor,
}}
>
<text content="Ctrl+C to exit | Press ? for keybindings" style={{ fg: "#888888" }} />
</box>
<diff
ref={(node) => {
// React clears refs before passive host destruction.
if (node) diffRef.current = node
}}
diff={exampleDiff}
view={view}
filetype="typescript"
syntaxStyle={syntaxStyle}
showLineNumbers={showLineNumbers}
wrapMode={wrapMode}
addedBg={theme.addedBg}
removedBg={theme.removedBg}
contextBg={theme.contextBg}
addedSignColor={theme.addedSignColor}
removedSignColor={theme.removedSignColor}
lineNumberFg={theme.lineNumberFg}
lineNumberBg={theme.lineNumberBg}
addedLineNumberBg={theme.addedLineNumberBg}
removedLineNumberBg={theme.removedLineNumberBg}
selectionBg={theme.selectionBg}
selectionFg={theme.selectionFg}
style={{
flexGrow: 1,
flexShrink: 1,
}}
/>
<HelpModal theme={theme} visible={showHelp} />
</box>
)
}
if (import.meta.main) {
const renderer = await createCliRenderer({ exitOnCtrlC: true, targetFps: 60 })
renderer.setBackgroundColor("#0D1117")
createRoot(renderer).render(<App />)
}