Solid.js 绑定

使用 Solid.js 的细粒度响应式系统和 OpenTUI 构建终端用户界面。

安装

bun install solid-js @opentui/solid

运行时支持

@opentui/solid 发布一个 npm 包,通过包的 exports 映射提供运行时特定的入口点。根包和 JSX 运行时子路径在 Bun 和 Node 中均可工作。Bun 特有的辅助工具仅限 Bun 使用,如果从 Node 导入会抛出清晰的运行时错误。

入口点BunNode说明
@opentui/solid主渲染器和测试辅助工具
@opentui/solid/jsx-runtime编译后 JSX 的真实运行时模块
@opentui/solid/jsx-dev-runtime编译后 JSX 的开发运行时模块
@opentui/solid/preloadBun 预加载钩子
@opentui/solid/bun-pluginBun 打包器/插件入口
@opentui/solid/runtime-plugin-supportBun 运行时加载器支持
@opentui/solid/runtime-plugin-support/configure无副作用的 Bun 运行时加载器支持

设置

1. 配置 TypeScript

tsconfig.json 中添加 JSX 配置:

{
  "compilerOptions": {
    "jsx": "preserve",
    "jsxImportSource": "@opentui/solid"
  }
}

2. 配置 Bun

bunfig.toml 中添加预加载脚本:

preload = ["@opentui/solid/preload"]

3. 配置 Node 编译

Node 支持目前针对编译后的 Solid TSX。使用 Solid 的通用转换语义,并将生成的运行时导入指向已发布的 solid-js JS 文件:

import ts from "@babel/preset-typescript"
import moduleResolver from "babel-plugin-module-resolver"
import solid from "babel-preset-solid"

export default {
  plugins: [
    [
      moduleResolver,
      {
        resolvePath(specifier) {
          if (specifier === "solid-js") return "solid-js/dist/solid.js"
          if (specifier === "solid-js/store") return "solid-js/store/dist/store.js"
          return specifier
        },
      },
    ],
  ],
  presets: [[solid, { moduleName: "@opentui/solid", generate: "universal" }], [ts]],
}

当前 Node 运行时路径仍比 Bun 更底层。OpenTUI 的原生渲染器目前依赖 Node 的实验性 FFI 支持,因此在运行前请确保使用 Node 26.3.0,并传递 --experimental-ffi--permission 以及应用所需的文件系统或 FFI 权限。OpenTUI 不会自动安装 Node。

4. 启用运行时加载插件支持(如需要)

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

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

此入口点仅限 Bun 使用。

5. 创建你的应用

import { render } from "@opentui/solid"

const App = () => <text>Hello, World!</text>

render(App)

使用 bun index.tsx 运行。

组件

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

注意: Solid 使用 snake_case 命名多词组件名(例如 ascii_fonttab_select)。

布局与显示

  • <text> - 带样式的文本容器
  • <box> - 带边框的布局容器
  • <scrollbox> - 可滚动容器
  • <ascii_font> - ASCII 艺术文本
  • <markdown> - 渲染 Markdown 内容

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

输入

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

代码与差异

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

文本修饰器

<text> 组件内使用:

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

API 参考

render(node, rendererOrConfig?)

将 Solid 组件树渲染到 CLI 渲染器中。

import { render } from "@opentui/solid"

// 简单用法
render(() => <App />)

// 使用渲染器配置
render(() => <App />, {
  targetFps: 30,
  exitOnCtrlC: false,
})

参数:

  • node - 返回 JSX 元素的函数
  • rendererOrConfig - 可选的 CliRenderer 实例或 CliRendererConfig

testRender(node, options?)

创建用于快照和交互测试的测试渲染器。

import { testRender } from "@opentui/solid"

const testSetup = await testRender(() => <App />, { width: 40, height: 10 })

extend(components)

将自定义 Renderable 注册为 JSX 内置元素。

import { extend } from "@opentui/solid"

extend({ custom_box: CustomBoxRenderable })

getComponentCatalogue()

返回驱动 JSX 标签查找的当前组件目录。

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

Scrollback 写入器

split-footer 模式下配合 externalOutputMode: "capture-stdout" 使用时,Solid 绑定提供辅助工具,将 JSX 渲染的输出追加到页脚上方。它们封装了 renderer.writeToScrollback,使你可以使用信号和组件编写 scrollback 内容。

writeSolidToScrollback(renderer, node, options?)

渲染一个 JSX 节点一次,并将其作为 scrollback 提交追加。

import { writeSolidToScrollback } from "@opentui/solid"

writeSolidToScrollback(renderer, () => <text fg="#8BD5CA">api responded in 12ms</text>)

createScrollbackWriter(node, options?)

如果你需要将相同的 JSX 渲染传递给多个 writeToScrollback 调用(或在自己的代码中持有写入器),请使用更底层的工厂函数:

import { createScrollbackWriter } from "@opentui/solid"

const writer = createScrollbackWriter(() => <text>logged at {new Date().toISOString()}</text>, { startOnNewLine: true })

renderer.writeToScrollback(writer)

选项

选项类型默认值说明
widthnumber渲染器宽度覆盖快照的列宽
heightnumber自动测量覆盖快照的行高(否则从布局测量)
rowColumnsnumber快照宽度用于尾部跟踪的显式末行列数
startOnNewLinebooleantrue如果上一个提交在行中间结束,则在此提交前插入换行
trailingNewlineboolean-在最后一行后追加换行

写入器返回一个 ScrollbackSnapshot,其 teardown 会在快照渲染后销毁内部的 Solid 子树。如果你需要流式提交来随时间重新渲染同一棵树,请直接使用核心 ScrollbackSurface API。

Hooks

useRenderer()

获取 OpenTUI 渲染器实例。

import { useRenderer } from "@opentui/solid"
import { onMount } from "solid-js"

const App = () => {
  const renderer = useRenderer()

  onMount(() => {
    renderer.console.show()
    console.log("Hello from console!")
  })

  return <box />
}

useKeyboard(handler, options?)

订阅键盘事件。

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

const App = () => {
  const renderer = useRenderer()

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

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

处理按键释放事件:

import { createSignal } from "solid-js"

const App = () => {
  const [pressedKeys, setPressedKeys] = createSignal(new Set<string>())

  useKeyboard(
    (event) => {
      setPressedKeys((keys) => {
        const newKeys = new Set(keys)
        if (event.eventType === "release") {
          newKeys.delete(event.name)
        } else {
          newKeys.add(event.name)
        }
        return newKeys
      })
    },
    { release: true },
  )

  return <text>Pressed: {Array.from(pressedKeys()).join(", ") || "none"}</text>
}

onResize(callback)

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

import { onResize } from "@opentui/solid"

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

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

onFocus(callback)

当终端窗口获得焦点时运行副作用。

import { onFocus } from "@opentui/solid"

const App = () => {
  onFocus(() => {
    console.log("Terminal focused")
  })

  return <text>Switch away and back to trigger focus events</text>
}

onBlur(callback)

当终端窗口失去焦点时运行副作用。

import { onBlur } from "@opentui/solid"

const App = () => {
  onBlur(() => {
    console.log("Terminal blurred")
  })

  return <text>Switch away and back to trigger blur events</text>
}

这些 hooks 在终端模拟器支持时监听终端的聚焦/失焦事件。

useTerminalDimensions()

获取响应式终端尺寸(返回 Solid signal)。

import { useTerminalDimensions } from "@opentui/solid"

const App = () => {
  const dimensions = useTerminalDimensions()

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

usePaste(handler)

订阅粘贴事件。

import { usePaste } from "@opentui/solid"

const textDecoder = new TextDecoder()

const App = () => {
  usePaste((event) => {
    console.log("Pasted:", textDecoder.decode(event.bytes))
  })

  return <text>Paste something!</text>
}

useSelectionHandler(callback)

处理文本选择事件。

import { useSelectionHandler } from "@opentui/solid"

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

  return <text selectable>Select me!</text>
}

useTimeline(options?)

创建和管理动画。

import { useTimeline } from "@opentui/solid"
import { createSignal, onMount } from "solid-js"

const App = () => {
  const [width, setWidth] = createSignal(0)

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

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

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

特殊组件

Portal

将子元素渲染到不同的挂载点(适用于模态框和覆盖层)。

import { Portal, useRenderer } from "@opentui/solid"

const App = () => {
  const renderer = useRenderer()

  return (
    <box>
      <text>Main content</text>
      <Portal mount={renderer.root}>
        <box border>Overlay</box>
      </Portal>
    </box>
  )
}

Dynamic

动态渲染任意内置元素或组件。

import { Dynamic } from "@opentui/solid"
import { createSignal } from "solid-js"

const App = () => {
  const [isMultiline, setIsMultiline] = createSignal(false)

  return <Dynamic component={isMultiline() ? "textarea" : "input"} />
}

生产构建

使用 Bun.build 配合 Solid 插件:

import solidPlugin from "@opentui/solid/bun-plugin"

await Bun.build({
  entrypoints: ["./index.tsx"],
  target: "bun",
  outdir: "./build",
  plugins: [solidPlugin],
})

编译为独立可执行文件:

await Bun.build({
  entrypoints: ["./index.tsx"],
  plugins: [solidPlugin],
  compile: {
    target: "bun-darwin-arm64",
    outfile: "./app-macos",
  },
})

如果该可执行文件在运行时加载外部插件/模块,请在应用入口中保留 import "@opentui/solid/runtime-plugin-support"

示例:计数器

import { render, useKeyboard, useRenderer } from "@opentui/solid"
import { createSignal } from "solid-js"

const App = () => {
  const [count, setCount] = createSignal(0)
  const renderer = useRenderer()

  useKeyboard((key) => {
    if (key.name === "up") setCount((c) => c + 1)
    if (key.name === "down") setCount((c) => c - 1)
    if (key.name === "escape") renderer.destroy()
  })

  return (
    <box border padding={2}>
      <text>Count: {count()}</text>
      <text fg="#888">Up/Down to change, ESC to close</text>
    </box>
  )
}

render(App)

与 React 绑定的差异

方面SolidReact
渲染函数render(() => <App />)createRoot(renderer).render(<App />)
组件命名snake_case(ascii_fontkebab-case(ascii-font
状态管理createSignaluseState
副作用onMountonCleanupuseEffect
大小调整 hookonResize(callback)useOnResize(callback)
尺寸返回 signal:dimensions().width返回对象:dimensions.width
额外 hooksonFocusonBlurusePasteuseSelectionHandler-
特殊组件PortalDynamic-