Files
2026-03-08 00:45:40 +01:00

283 lines
8.3 KiB
TypeScript

import { createCliRenderer, parseColor, SyntaxStyle } from "@opentui/core"
import { createRoot, useKeyboard } from "@opentui/react"
import { useState, useMemo } 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 [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]
const syntaxStyle = useMemo(() => SyntaxStyle.fromStyles(theme.syntaxStyle), [theme])
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)
}
})
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
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 />)
}