React 绑定

使用 React 熟悉的模式和组件构建终端用户界面。

安装

使用 buncreate-tui 快速开始:

bun create tui --template react

手动安装:

bun install @opentui/react @opentui/core react

快速开始

import { createCliRenderer } from "@opentui/core"
import { createRoot } from "@opentui/react"

function App() {
  return <text>Hello, world!</text>
}

const renderer = await createCliRenderer()
createRoot(renderer).render(<App />)

TypeScript 配置

配置你的 tsconfig.json

{
  "compilerOptions": {
    "lib": ["ESNext", "DOM"],
    "target": "ESNext",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "jsx": "react-jsx",
    "jsxImportSource": "@opentui/react",
    "strict": true,
    "skipLibCheck": true
  }
}

运行时加载插件支持(如需要)

如果你的应用在运行时加载外部 TS/TSX 模块(例如基于文件的插件系统),请在动态导入之前在应用入口处导入一次:

import "@opentui/react/runtime-plugin-support"

此配置同时适用于常规 Bun 运行和独立编译的可执行文件。

组件

OpenTUI React 提供了映射到核心 Renderable 的 JSX 内置元素:

布局与显示

  • <text> - 带样式的文本显示
  • <box> - 带边框和布局的容器
  • <scrollbox> - 可滚动容器
  • <ascii-font> - ASCII 艺术文本

二维码支持来自 @opentui/qrcode/react,必须通过 registerQRCode() 显式注册。

输入

  • <input> - 单行文本输入
  • <textarea> - 多行文本输入
  • <select> - 选择列表
  • <tab-select> - 基于标签的选择

代码与差异

  • <code> - 语法高亮代码
  • <line-number> - 支持差异/诊断的行号
  • <diff> - 统一或分栏差异查看器
  • <markdown> - Markdown 渲染

文本修饰器

<text> 组件内使用:

  • <span> - 内联样式文本
  • <strong><b> - 粗体文本
  • <em><i> - 斜体文本
  • <u> - 下划线文本
  • <br> - 换行
  • <a> - 链接文本

API 参考

createRoot(renderer)

创建一个 React 根节点,用于渲染到终端。

import { createCliRenderer } from "@opentui/core"
import { createRoot } from "@opentui/react"

const renderer = await createCliRenderer()
createRoot(renderer).render(<App />)

关于插件插槽,请参阅插件插槽概览React 插槽

Hooks

useRenderer()

获取 OpenTUI 渲染器实例。

import { useRenderer } from "@opentui/react"
import { useEffect } from "react"

function App() {
  const renderer = useRenderer()

  useEffect(() => {
    renderer.console.show()
    console.log("Hello from console!")
  }, [])

  return <box />
}

useKeyboard(handler, options?)

处理键盘事件。

import { useKeyboard, useRenderer } from "@opentui/react"

function App() {
  const renderer = useRenderer()

  useKeyboard((key) => {
    if (key.name === "escape") {
      renderer.destroy()
    }
  })

  return <text>Press ESC to close</text>
}

处理按键释放事件:

useKeyboard(
  (event) => {
    if (event.eventType === "release") {
      console.log("Key released:", event.name)
    } else {
      console.log("Key pressed:", event.name)
    }
  },
  { release: true },
)

useOnResize(callback)

处理终端窗口大小调整事件。

import { useOnResize } from "@opentui/react"

function App() {
  useOnResize((width, height) => {
    console.log(`Resized to ${width}x${height}`)
  })

  return <text>Resize-aware component</text>
}

useTerminalDimensions()

获取响应式终端尺寸。

import { useTerminalDimensions } from "@opentui/react"

function App() {
  const { width, height } = useTerminalDimensions()

  return (
    <text>
      Terminal: {width}x{height}
    </text>
  )
}

usePaste(handler)

处理终端粘贴事件(括号粘贴模式)。

import { decodePasteBytes } from "@opentui/core"
import { usePaste } from "@opentui/react"

function App() {
  usePaste((event) => {
    const text = decodePasteBytes(event.bytes)
    console.log("Pasted text:", text)
  })

  return <text>Paste something into the terminal</text>
}

useFocus(handler)

订阅终端窗口获得焦点事件。

import { useFocus } from "@opentui/react"

function App() {
  useFocus(() => {
    console.log("Terminal gained focus")
  })

  return <text>Focus-aware component</text>
}

useBlur(handler)

订阅终端窗口失去焦点事件。

import { useBlur } from "@opentui/react"

function App() {
  useBlur(() => {
    console.log("Terminal lost focus")
  })

  return <text>Blur-aware component</text>
}

useSelectionHandler(handler)

处理文本选择事件(例如鼠标拖拽选择)。

import { useSelectionHandler } from "@opentui/react"

function App() {
  useSelectionHandler((selection) => {
    const text = selection.getSelectedText()
    console.log("Selected:", text)
  })

  return <text selectable>Select this text with your mouse</text>
}

useTimeline(options?)

创建和管理动画。

import { useTimeline } from "@opentui/react"
import { useEffect, useState } from "react"

function App() {
  const [width, setWidth] = useState(0)

  const timeline = useTimeline({
    duration: 2000,
    loop: false,
  })

  useEffect(() => {
    timeline.add(
      { width },
      {
        width: 50,
        duration: 2000,
        ease: "linear",
        onUpdate: (animation) => {
          setWidth(animation.targets[0].width)
        },
      },
    )
  }, [])

  return <box style={{ width, backgroundColor: "#6a5acd" }} />
}

选项:

  • duration - 动画持续时间,单位为毫秒(默认:1000)
  • loop - 是否循环(默认:false)
  • autoplay - 自动开始(默认:true)
  • onComplete - 完成回调
  • onPause - 暂停回调

样式

通过属性或 style 属性为组件设置样式:

// 直接使用属性
<box backgroundColor="blue" padding={2}>
  <text>Hello</text>
</box>

// 使用 style 属性
<box style={{ backgroundColor: "blue", padding: 2 }}>
  <text>Hello</text>
</box>

示例:登录表单

import { createCliRenderer } from "@opentui/core"
import { createRoot, useKeyboard } from "@opentui/react"
import { useCallback, useState } from "react"

function App() {
  const [username, setUsername] = useState("")
  const [password, setPassword] = useState("")
  const [focused, setFocused] = useState<"username" | "password">("username")
  const [status, setStatus] = useState("idle")

  useKeyboard((key) => {
    if (key.name === "tab") {
      setFocused((prev) => (prev === "username" ? "password" : "username"))
    }
  })

  const handleSubmit = useCallback(() => {
    if (username === "admin" && password === "secret") {
      setStatus("success")
    } else {
      setStatus("error")
    }
  }, [username, password])

  return (
    <box style={{ border: true, padding: 2, flexDirection: "column", gap: 1 }}>
      <text fg="#FFFF00">Login Form</text>

      <box title="Username" style={{ border: true, width: 40, height: 3 }}>
        <input
          placeholder="Enter username..."
          onInput={setUsername}
          onSubmit={handleSubmit}
          focused={focused === "username"}
        />
      </box>

      <box title="Password" style={{ border: true, width: 40, height: 3 }}>
        <input
          placeholder="Enter password..."
          onInput={setPassword}
          onSubmit={handleSubmit}
          focused={focused === "password"}
        />
      </box>

      <text fg={status === "success" ? "green" : status === "error" ? "red" : "#999"}>{status.toUpperCase()}</text>
    </box>
  )
}

const renderer = await createCliRenderer()
createRoot(renderer).render(<App />)

组件扩展

将自定义 Renderable 注册为 JSX 元素:

import { BoxRenderable, createCliRenderer, type BoxOptions, type RenderContext } from "@opentui/core"
import { createRoot, extend } from "@opentui/react"

class ConsoleButtonRenderable extends BoxRenderable {
  private _label: string = "Button"

  constructor(ctx: RenderContext, options: BoxOptions & { label?: string }) {
    super(ctx, options)
    if (options.label) this._label = options.label
    this.borderStyle = "single"
    this.padding = 2
  }

  get label(): string {
    return this._label
  }

  set label(value: string) {
    this._label = value
    this.requestRender()
  }
}

// 添加 TypeScript 支持
declare module "@opentui/react" {
  interface OpenTUIComponents {
    consoleButton: typeof ConsoleButtonRenderable
  }
}

// 注册组件
extend({ consoleButton: ConsoleButtonRenderable })

// 在 JSX 中使用
function App() {
  return <consoleButton label="Click me!" style={{ border: true, backgroundColor: "green" }} />
}

const renderer = await createCliRenderer()
createRoot(renderer).render(<App />)

React DevTools

OpenTUI React 支持使用 React DevTools 进行调试:

  1. 安装:
bun add --dev react-devtools-core@7
  1. 启动 DevTools:
npx react-devtools@7
  1. 使用 DEV 标志运行:
DEV=true bun run your-app.ts