feat: add support for transparent terminal background rendering (#225)

This addresses https://github.com/sst/opentui/issues/184 by defaulting
to terminal background if renderer alpha channel is 0.

The transparent background can be set by using
`renderer.setBackgroundColor("transparent")` for example in
`packages/react/examples/basic.tsx`.
```diff
import { bold, fg, italic, t, TextAttributes } from "@opentui/core"
import { render, useKeyboard, useRenderer } from "@opentui/react"
import { useCallback, useState } from "react"

export const App = () => {
  const renderer = useRenderer()
+  renderer.setBackgroundColor("transparent")
  const [username, setUsername] = useState("")
  const [password, setPassword] = useState("")
```

Example in ghostty
<img width="852" height="670" alt="image"
src="https://github.com/user-attachments/assets/658703ad-77a6-4624-b07c-5a24bbf8447c"
/>

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
KeremP
2025-10-25 11:25:18 -04:00
committed by GitHub
parent 7de7e65c5c
commit dd87918e3d
3 changed files with 55 additions and 8 deletions
+3 -2
View File
@@ -67,6 +67,7 @@ export interface CliRendererConfig {
useConsole?: boolean
experimental_splitHeight?: number
useKittyKeyboard?: boolean
backgroundColor?: ColorInput
}
export type PixelResolution = {
@@ -230,7 +231,7 @@ export class CliRenderer extends EventEmitter implements RenderContext {
private frameTimes: number[] = []
private maxStatSamples: number = 300
private postProcessFns: ((buffer: OptimizedBuffer, deltaTime: number) => void)[] = []
private backgroundColor: RGBA = RGBA.fromHex("#000000")
private backgroundColor: RGBA = RGBA.fromInts(0, 0, 0, 0)
private waitingForPixelResolution: boolean = false
private rendering: boolean = false
@@ -1030,7 +1031,7 @@ export class CliRenderer extends EventEmitter implements RenderContext {
this.renderOffset = height - this._splitHeight
this.width = width
this.height = this._splitHeight
this.currentRenderBuffer.clear(RGBA.fromHex("#000000"))
this.currentRenderBuffer.clear(this.backgroundColor)
this.lib.setRenderOffset(this.rendererPtr, this.renderOffset)
} else {
this.width = width
+13 -6
View File
@@ -178,7 +178,7 @@ pub const CliRenderer = struct {
.currentRenderBuffer = currentBuffer,
.nextRenderBuffer = nextBuffer,
.pool = pool,
.backgroundColor = .{ 0.0, 0.0, 0.0, 1.0 },
.backgroundColor = .{ 0.0, 0.0, 0.0, 0.0 },
.renderOffset = 0,
.terminal = Terminal.init(.{}),
.testing = testing,
@@ -216,8 +216,8 @@ pub const CliRenderer = struct {
.hitGridHeight = height,
};
try currentBuffer.clear(.{ self.backgroundColor[0], self.backgroundColor[1], self.backgroundColor[2], 1.0 }, CLEAR_CHAR);
try nextBuffer.clear(.{ self.backgroundColor[0], self.backgroundColor[1], self.backgroundColor[2], 1.0 }, null);
try currentBuffer.clear(.{ self.backgroundColor[0], self.backgroundColor[1], self.backgroundColor[2], self.backgroundColor[3] }, CLEAR_CHAR);
try nextBuffer.clear(.{ self.backgroundColor[0], self.backgroundColor[1], self.backgroundColor[2], self.backgroundColor[3] }, null);
return self;
}
@@ -386,7 +386,7 @@ pub const CliRenderer = struct {
try self.nextRenderBuffer.resize(width, height);
try self.currentRenderBuffer.clear(.{ 0.0, 0.0, 0.0, 1.0 }, CLEAR_CHAR);
try self.nextRenderBuffer.clear(.{ self.backgroundColor[0], self.backgroundColor[1], self.backgroundColor[2], 1.0 }, null);
try self.nextRenderBuffer.clear(.{ self.backgroundColor[0], self.backgroundColor[1], self.backgroundColor[2], self.backgroundColor[3] }, null);
const newHitGridSize = width * height;
const currentHitGridSize = self.hitGridWidth * self.hitGridHeight;
@@ -589,9 +589,16 @@ pub const CliRenderer = struct {
const bgR = rgbaComponentToU8(cell.bg[0]);
const bgG = rgbaComponentToU8(cell.bg[1]);
const bgB = rgbaComponentToU8(cell.bg[2]);
const bgA = cell.bg[3];
ansi.ANSI.fgColorOutput(writer, fgR, fgG, fgB) catch {};
ansi.ANSI.bgColorOutput(writer, bgR, bgG, bgB) catch {};
// If alpha is 0 (transparent), use terminal default background instead of black
if (bgA < 0.001) {
writer.writeAll("\x1b[49m") catch {};
} else {
ansi.ANSI.bgColorOutput(writer, bgR, bgG, bgB) catch {};
}
ansi.TextAttributes.applyAttributesOutputWriter(writer, cell.attributes) catch {};
}
@@ -686,7 +693,7 @@ pub const CliRenderer = struct {
self.renderStats.cellsUpdated = cellsUpdated;
self.renderStats.renderTime = renderTime;
self.nextRenderBuffer.clear(.{ self.backgroundColor[0], self.backgroundColor[1], self.backgroundColor[2], 1.0 }, null) catch {};
self.nextRenderBuffer.clear(.{ self.backgroundColor[0], self.backgroundColor[1], self.backgroundColor[2], self.backgroundColor[3] }, null) catch {};
const temp = self.currentHitGrid;
self.currentHitGrid = self.nextHitGrid;
+39
View File
@@ -128,6 +128,45 @@ if [ "$LINK_REACT" = true ]; then
else
echo "Warning: $REACT_PATH not found"
fi
# Only link react, react-dom, and react-reconciler when not in copy mode
if [ "$COPY_MODE" = false ]; then
# Link react
remove_if_exists "$NODE_MODULES_DIR/react"
if [ -d "$OPENTUI_ROOT/node_modules/react" ]; then
ln -s "$OPENTUI_ROOT/node_modules/react" "$NODE_MODULES_DIR/react"
echo "✓ Linked react"
elif [ -d "$OPENTUI_ROOT/packages/react/node_modules/react" ]; then
ln -s "$OPENTUI_ROOT/packages/react/node_modules/react" "$NODE_MODULES_DIR/react"
echo "✓ Linked react (from packages/react/node_modules)"
else
echo "Warning: react not found in OpenTUI node_modules"
fi
# Link react-dom
remove_if_exists "$NODE_MODULES_DIR/react-dom"
if [ -d "$OPENTUI_ROOT/node_modules/react-dom" ]; then
ln -s "$OPENTUI_ROOT/node_modules/react-dom" "$NODE_MODULES_DIR/react-dom"
echo "✓ Linked react-dom"
elif [ -d "$OPENTUI_ROOT/packages/react/node_modules/react-dom" ]; then
ln -s "$OPENTUI_ROOT/packages/react/node_modules/react-dom" "$NODE_MODULES_DIR/react-dom"
echo "✓ Linked react-dom (from packages/react/node_modules)"
else
echo "Warning: react-dom not found in OpenTUI node_modules"
fi
# Link react-reconciler
remove_if_exists "$NODE_MODULES_DIR/react-reconciler"
if [ -d "$OPENTUI_ROOT/node_modules/react-reconciler" ]; then
ln -s "$OPENTUI_ROOT/node_modules/react-reconciler" "$NODE_MODULES_DIR/react-reconciler"
echo "✓ Linked react-reconciler"
elif [ -d "$OPENTUI_ROOT/packages/react/node_modules/react-reconciler" ]; then
ln -s "$OPENTUI_ROOT/packages/react/node_modules/react-reconciler" "$NODE_MODULES_DIR/react-reconciler"
echo "✓ Linked react-reconciler (from packages/react/node_modules)"
else
echo "Warning: react-reconciler not found in OpenTUI node_modules"
fi
fi
fi
# Link Solid and solid-js if requested