These docs describe the main branch, including unreleased changes. Read the 0.8.0 docs.

Code

Code displays source text with Tree-sitter syntax highlighting. Use Markdown for documents or Diff for changed lines.

Availability

FieldAvailability
Package@opentui/core
Core renderableCodeRenderable
React<code> (automatic)
Solid<code> (automatic)
StatusBuilt in

Basic usage

Renderable API

OpenTUI preview

Run this example to open an interactive terminal.

Source code · typescript
import { CodeRenderable, createCliRenderer, SyntaxStyle, RGBA } from "@opentui/core"

const renderer = await createCliRenderer()

const syntaxStyle = SyntaxStyle.fromStyles(
  {
    default: { fg: RGBA.defaultForeground() },
    keyword: { fg: RGBA.defaultForeground(), bold: true },
    string: { fg: RGBA.fromIndex(247) },
    comment: { fg: RGBA.fromIndex(244), italic: true },
  },
  renderer.nativeScene,
)

const code = new CodeRenderable(renderer, {
  id: "code",
  content: `function hello() {
  // This is a comment
  const message = "Hello, world!"
  return message
}`,
  filetype: "javascript",
  syntaxStyle,
  width: 50,
  height: 10,
})

renderer.root.add(code)
function hello() {
  // This is a comment
  const message = "Hello, world!"
  return message
}

Creating syntax styles

SyntaxStyle.fromStyles(definitions, owner) maps syntax token names to colors and attributes. It creates a native style, so it needs an owner. Pass renderer.nativeScene. Without a renderer, pass a ResourceContext.

fromStyles() and registerStyle() copy each definition, including its colors. getStyle(), getAllStyles(), and mergeStyles() return copies. Changes to an input object or a returned value do not change the registered style. To change a style, call registerStyle() again.

import { SyntaxStyle, RGBA, parseColor } from "@opentui/core"

const syntaxStyle = SyntaxStyle.fromStyles(
  {
    // Basic tokens
    keyword: { fg: RGBA.fromHex("#FF7B72"), bold: true },
    "keyword.import": { fg: RGBA.fromHex("#FF7B72"), bold: true },
    "keyword.operator": { fg: RGBA.fromHex("#FF7B72") },

    string: { fg: RGBA.fromHex("#A5D6FF") },
    comment: { fg: RGBA.fromHex("#8B949E"), italic: true },
    number: { fg: RGBA.fromHex("#79C0FF") },
    boolean: { fg: RGBA.fromHex("#79C0FF") },
    constant: { fg: RGBA.fromHex("#79C0FF") },

    // Functions and types
    function: { fg: RGBA.fromHex("#D2A8FF") },
    "function.call": { fg: RGBA.fromHex("#D2A8FF") },
    "function.method.call": { fg: RGBA.fromHex("#D2A8FF") },
    type: { fg: RGBA.fromHex("#FFA657") },
    constructor: { fg: RGBA.fromHex("#FFA657") },

    // Variables and properties
    variable: { fg: RGBA.fromHex("#E6EDF3") },
    "variable.member": { fg: RGBA.fromHex("#79C0FF") },
    property: { fg: RGBA.fromHex("#79C0FF") },

    // Operators and punctuation
    operator: { fg: RGBA.fromHex("#FF7B72") },
    punctuation: { fg: RGBA.fromHex("#F0F6FC") },
    "punctuation.bracket": { fg: RGBA.fromHex("#F0F6FC") },
    "punctuation.delimiter": { fg: RGBA.fromHex("#C9D1D9") },

    // Default fallback
    default: { fg: RGBA.fromHex("#E6EDF3") },
  },
  renderer.nativeScene,
)

A style belongs to one owner. To use the same definitions in another renderer, create a new style for that renderer. Destroy the new style when that renderer no longer needs it:

const definitions = syntaxStyle.getAllStyles()
const boundStyle = SyntaxStyle.fromStyles(definitions, destination.nativeScene)
// Use boundStyle in the destination's Code or Markdown nodes.
// After you destroy those nodes:
boundStyle.destroy()

CodeRenderable does not destroy the syntaxStyle you pass. You destroy it. Text buffers and edit buffers accept only a style with the same owner. A style from another owner throws an owner-mismatch error. Core does not copy it.

getAllStyles() returns only registered definitions. It does not return the styles that native code creates for text chunks. Pass its Map directly to fromStyles() to keep the registration order, including numeric names. An object lists numeric names first, which can change style IDs.

Style properties

Each style definition can include:

PropertyTypeDescription
fgRGBAForeground (text) color
bgRGBABackground color
boldbooleanBold text
italicbooleanItalic text
underlinebooleanUnderlined text
dimbooleanDimmed text

Supported languages

OpenTUI bundles Tree-sitter parsers for these languages:

  • JavaScript and JSX

  • TypeScript and TSX

  • Markdown and Markdown inline

  • Zig

Other grammars require configuration. See the Tree-sitter reference.

Streaming mode

Enable streaming mode when content arrives incrementally, like LLM output:

const code = new CodeRenderable(renderer, {
  id: "streaming-code",
  content: "",
  filetype: "typescript",
  syntaxStyle,
  streaming: true, // Enable streaming mode
})

// Later, append content
code.content += "const x = 1\n"
code.content += "const y = 2\n"

With drawUnstyledText: false, streaming mode changes what Code shows while a highlight runs:

  • Without streaming, Code shows nothing until each highlight completes.

  • With streaming, Code shows nothing until the first highlight completes. Later updates keep the previous highlighted text visible until the new highlight completes.

With drawUnstyledText: true, a content change shows the new text unstyled while its highlight runs, with or without streaming. In streaming mode, after the first highlight, other changes that start a highlight, such as filetype or conceal, keep the previous highlighted text visible. Each highlight processes the complete current content.

Text selection

Text selection is on by default. Set selectable: false to turn it off. Use selectionBg and selectionFg to set selection colors:

const code = new CodeRenderable(renderer, {
  id: "code",
  content: sourceCode,
  filetype: "typescript",
  syntaxStyle,
  selectable: true,
  selectionBg: "#264F78",
  selectionFg: "#FFFFFF",
})

Concealment

Set conceal to hide concealed syntax elements, such as Markdown formatting characters:

const code = new CodeRenderable(renderer, {
  id: "markdown",
  content: "# Heading\n**bold** text",
  filetype: "markdown",
  syntaxStyle,
  conceal: true, // Hide formatting characters
})

With line numbers

Use LineNumberRenderable to add line numbers:

import { CodeRenderable, LineNumberRenderable, RGBA, ScrollBoxRenderable, SyntaxStyle } from "@opentui/core"

const sourceCode = "const answer = 42\nconsole.log(answer)\n"
const syntaxStyle = SyntaxStyle.fromStyles(
  {
    default: { fg: RGBA.fromHex("#E6EDF3") },
  },
  renderer.nativeScene,
)

const code = new CodeRenderable(renderer, {
  id: "code",
  content: sourceCode,
  filetype: "typescript",
  syntaxStyle,
  width: "100%",
})

const lineNumbers = new LineNumberRenderable(renderer, {
  id: "code-with-lines",
  target: code,
  minWidth: 3,
  paddingRight: 1,
  fg: "#6b7280",
  bg: "#161b22",
  width: "100%",
})

// Wrap in ScrollBox for scrolling
const scrollbox = new ScrollBoxRenderable(renderer, {
  id: "scrollbox",
  width: 60,
  height: 20,
})
scrollbox.add(lineNumbers)
renderer.root.add(scrollbox)

Properties

PropertyTypeDefaultDescription
contentstring""Source code to display
filetypestring-Language for syntax highlighting
syntaxStyleSyntaxStylerequiredSyntax style for tokens
streamingbooleanfalseChange what shows while a highlight runs (see Streaming mode)
concealbooleantrueHide concealed syntax elements
drawUnstyledTextbooleantrueShow unstyled text while a highlight runs (see Streaming mode)
treeSitterClientTreeSitterClient-Custom Tree-sitter client instance

When the renderable has a filetype, a change to content, filetype, syntaxStyle, conceal, drawUnstyledText, streaming, or treeSitterClient starts a new highlight. Code starts each highlight in its update hook (onUpdate) during the next frame. Code does not start a highlight while it or an ancestor has visible: false.

Inherited from TextBufferRenderable

PropertyTypeDefaultDescription
fgstring | RGBA#FFFFFFDefault foreground color
bgstring | RGBAtransparentBackground color
selectablebooleantrueWhether text selection is on
selectionBgstring | RGBA-Selection background color
selectionFgstring | RGBA-Selection foreground color
wrapModestring"word"Text wrapping: "none", "char", "word"
textAlign"left" | "center" | "right""left"Per-line alignment within the text width
tabIndicatorstring | number-Tab display character or width

Additional properties

PropertyTypeDescription
lineCountnumberNumber of lines in the current text buffer
scrollYnumberCurrent vertical scroll position (get/set)
scrollXnumberCurrent horizontal scroll position (get/set)
scrollWidthnumberTotal scrollable width (read-only)
scrollHeightnumberTotal scrollable height (read-only)
isHighlightingbooleanWhether highlighting is in progress
plainTextstringPlain text in the current text buffer. It can differ from content after concealment or onChunks transforms

Markdown styles

For markdown highlighting, use markup-prefixed style names:

const markdownStyle = SyntaxStyle.fromStyles(
  {
    "markup.heading": { fg: RGBA.fromHex("#58A6FF"), bold: true },
    "markup.heading.1": { fg: RGBA.fromHex("#00FF88"), bold: true, underline: true },
    "markup.heading.2": { fg: RGBA.fromHex("#00D7FF"), bold: true },
    "markup.bold": { fg: RGBA.fromHex("#F0F6FC"), bold: true },
    "markup.strong": { fg: RGBA.fromHex("#F0F6FC"), bold: true },
    "markup.italic": { fg: RGBA.fromHex("#F0F6FC"), italic: true },
    "markup.list": { fg: RGBA.fromHex("#FF7B72") },
    "markup.quote": { fg: RGBA.fromHex("#8B949E"), italic: true },
    "markup.raw": { fg: RGBA.fromHex("#A5D6FF") },
    "markup.raw.block": { fg: RGBA.fromHex("#A5D6FF") },
    "markup.link": { fg: RGBA.fromHex("#58A6FF"), underline: true },
    "markup.link.url": { fg: RGBA.fromHex("#58A6FF"), underline: true },
    default: { fg: RGBA.fromHex("#E6EDF3") },
  },
  renderer.nativeScene,
)

Use Markdown for document structure and fenced code. Add a Line number gutter to a code renderable. Use Diff for unified or split patches. Use TextTable for tabular content without syntax parsing.

Read Text and terminal cells for wrapping, graphemes, and display-cell widths.

Changes0.8.0, 0.7.0
0.8.0
core: Each row of a long wrapped line, or of a long highlighted line, finds its first cluster and its style span by binary search. The bottom 30 rows of a 131,072-column highlighted line take 480 steps instead of 1.9 million. (#1781, #1796)
0.7.0
core: The protected TextBufferRenderable._textBufferSyntaxStyle field is removed. Styled text keeps its styles in the text buffer, so remove code that reads the field. (#1775)
core: A text buffer keeps its styled-text styles by value, and a Code creates two native objects instead of three. A highlight update no longer copies the style table or scans the object table. (#1768, #1775)