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,
Codeshows nothing until each highlight completes. - With streaming,
Codeshows 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,
)Related components#
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.