Vue.js support for OpenTUI.
bun install
bun run start:examplemkdir my-tui-app && cd my-tui-app
bun init -ybun add vue @opentui/core @opentui/vuecreate env.d.ts file at root of project
declare module "*.vue" {
import { DefineComponent } from "vue"
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
const component: DefineComponent<{}, {}, any>
export default component
}<!-- App.vue -->
<script setup lang="ts">
</script>
<template>
<textRenderable :style="{ fg: '#ff0000' }">
Hello World
</textRenderable>
</template>// index.ts
import { createApp } from "vue"
import { render } from "@opentui/vue"
import App from "./App.vue"
render(createApp(App))bun add -D bun-plugin-vue3// build.ts
import { pluginVue3 } from "bun-plugin-vue3"
const result = await Bun.build({
entrypoints: ["./index.ts"],
outdir: "./dist",
target: "bun",
plugins: [pluginVue3()],
})
if (!result.success) {
console.error("Build failed")
for (const message of result.logs) {
console.error(message)
}
process.exit(1)
}bun run build.tsbun run dist/index.jsImportant Note on
The component only accepts plain text as a direct child. For styled text or text chunks, you must use the content prop.
<script setup lang="ts">
import { blue, bold, t, underline, type TextChunk } from "@opentui/core"
const styledText = t`This is ${underline(blue("styled"))} text.`
const textChunk: TextChunk = bold(`This is a text chunk.`)
</script>
<template>
<textRenderable :content="styledText" />
<textRenderable :content="textChunk" />
<textRenderable>This is plain text.</textRenderable>
</template>@opentui/vue provides a set of composables to interact with the terminal and respond to events.
This composable returns the underlying CliRenderer instance from @opentui/core.
import { useCliRenderer } from "@opentui/vue"
const renderer = useCliRenderer()Listen to keypress events in your components.
<script setup lang="ts">
import { ref } from "vue"
import { useKeyboard } from "@opentui/vue"
import type { KeyEvent } from "@opentui/core"
const lastKey = ref("")
useKeyboard((key: KeyEvent) => {
lastKey.value = key.name
})
</script>
<template>
<textRenderable>
Last key pressed: {{ lastKey }}
</textRenderable>
</template>Execute a callback function whenever the terminal window is resized.
<script setup lang="ts">
import { useOnResize } from "@opentui/vue"
useOnResize((width, height) => {
console.log(`Terminal resized to ${width}x${height}`)
})
</script>Get the current terminal dimensions as a reactive object. The dimensions will automatically update when the terminal is resized.
<script setup lang="ts">
import { useTerminalDimensions } from "@opentui/vue"
const dimensions = useTerminalDimensions()
</script>
<template>
<textRenderable>
Width: {{ dimensions.width }}, Height: {{ dimensions.height }}
</textRenderable>
</template>@opentui/vue allows you to create and use your own custom components in your TUI applications. This is useful for creating reusable UI elements with specific styles and behaviors.
The extend function is used to register your custom components with the Vue renderer.
Here's a step-by-step guide to creating a custom button component.
First, create a class that extends one of the base renderables from @opentui/core. For our custom button, we'll extend BoxRenderable.
CustomButtonRenderable.ts:
import { BoxRenderable, OptimizedBuffer, RGBA, type BoxOptions, type RenderContext } from "@opentui/core"
export 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
}
// Set some default styling for buttons
this.borderStyle = "single"
this.padding = 2
}
protected override renderSelf(buffer: OptimizedBuffer): void {
super.renderSelf(buffer)
const centerX = this.x + Math.floor(this.width / 2 - this._label.length / 2)
const centerY = this.y + Math.floor(this.height / 2)
buffer.drawText(this._label, centerX, centerY, RGBA.fromInts(255, 255, 255, 255))
}
get label(): string {
return this._label
}
set label(value: string) {
this._label = value
this.requestRender()
}
}In your application's entry point (e.g., main.ts), import the extend function and your custom component. Then, call extend with an object where the key is the component's tag name (in camelCase) and the value is the component class.
main.ts:
import { render, extend } from "@opentui/vue"
import { ConsoleButtonRenderable } from "./CustomButtonRenderable"
import App from "./App.vue"
// Register the custom component
extend({ consoleButtonRenderable: ConsoleButtonRenderable })
// Render the app
render(App)Important: The
extendfunction should be called in your main application entry file (e.g.,main.tsorindex.js). Calling it inside the<script>section of a.vuefile can cause issues with the Vue compiler. It may incorrectly try to instantiate the renderable classes you import from@opentui/core, leading to a runtime error.
To get proper type-checking and autocompletion for your custom component in Vue templates, you need to augment the @opentui/vue types. Create a declaration file (e.g., opentui.d.ts) in your project and add the following:
opentui.d.ts:
import { ConsoleButtonRenderable } from "./CustomButtonRenderable"
declare module "@opentui/vue" {
export interface OpenTUIComponents {
consoleButtonRenderable: typeof ConsoleButtonRenderable
}
}Note: Make sure this file is included in your tsconfig.json.
Now you can use <consoleButtonRenderable> in your Vue components just like any other OpenTUI component.
ExtendExample.vue:
<template>
<boxRenderable :style="{ flexDirection: 'column' }">
<textRenderable>Custom Button Example</textRenderable>
<consoleButtonRenderable
label="Another Button"
:style="{
backgroundColor: 'green',
}"
/>
</boxRenderable>
</template>
<script setup lang="ts"></script>