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

Code

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

Availability#

Field Availability
Package @opentui/core
Core renderable CodeRenderable
React <code> (automatic)
Solid <code> (automatic)
Status Built in

Basic usage#

Renderable API#

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)

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:

Property Type Description
fg RGBA Foreground (text) color
bg RGBA Background color
bold boolean Bold text
italic boolean Italic text
underline boolean Underlined text
dim boolean Dimmed 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#

Property Type Default Description
content string "" Source code to display
filetype string - Language for syntax highlighting
syntaxStyle SyntaxStyle required Syntax style for tokens
streaming boolean false Change what shows while a highlight runs (see Streaming mode)
conceal boolean true Hide concealed syntax elements
drawUnstyledText boolean true Show unstyled text while a highlight runs (see Streaming mode)
treeSitterClient TreeSitterClient - 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#

Property Type Default Description
fg string | RGBA #FFFFFF Default foreground color
bg string | RGBA transparent Background color
selectable boolean true Whether text selection is on
selectionBg string | RGBA - Selection background color
selectionFg string | RGBA - Selection foreground color
wrapMode string "word" Text wrapping: "none", "char", "word"
textAlign "left" | "center" | "right" "left" Per-line alignment within the text width
tabIndicator string | number - Tab display character or width

Additional properties#

Property Type Description
lineCount number Number of lines in the current text buffer
scrollY number Current vertical scroll position (get/set)
scrollX number Current horizontal scroll position (get/set)
scrollWidth number Total scrollable width (read-only)
scrollHeight number Total scrollable height (read-only)
isHighlighting boolean Whether highlighting is in progress
plainText string Plain 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.6.0, 0.5.11, 0.5.2
0.6.0
Added CodeRenderable.destroyOwnedResources, CodeRenderable.onUpdate, CodeRenderable.nativeIntegration.
Removed CodeRenderable.destroy.
0.5.11
core: TextBufferRenderable.getLineSources() and CodeRenderable.getLineSources() return the source IDs of a range of visual lines. detectLinks() and ChunkRenderContext accept optional sourceRanges. (#1442, #1462)
core: CodeRenderable.updateStreamingPreview(content, initialStyledText) updates streamed code with a styled preview. (#1442)
Added CodeRenderable.getLineSources, CodeRenderable.updateStreamingPreview, ChunkRenderContext.sourceRanges.
0.5.2
core: CodeRenderable no longer creates unbounded Tree-sitter worker work during streaming. It runs one highlight at a time and keeps only the latest pending content. Highlight settlement now includes queued reruns, so rendered output and scrollback match the current content. CodeRenderable.destroy() is now exported. (#1331)
Added CodeRenderable.destroy.