Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions packages/core/src/buffer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,8 @@ export class OptimizedBuffer {

this._rawBuffers = {
char: new Uint32Array(toArrayBuffer(charPtr, 0, size * 4)),
fg: new Float32Array(toArrayBuffer(fgPtr, 0, size * 4 * 4)),
bg: new Float32Array(toArrayBuffer(bgPtr, 0, size * 4 * 4)),
fg: new Float32Array(toArrayBuffer(fgPtr, 0, size * 5 * 4)),
bg: new Float32Array(toArrayBuffer(bgPtr, 0, size * 5 * 4)),
attributes: new Uint32Array(toArrayBuffer(attributesPtr, 0, size * 4)),
}
}
Expand Down Expand Up @@ -174,8 +174,8 @@ export class OptimizedBuffer {
for (let x = 0; x < this._width; x++) {
const i = y * this._width + x
const cp = char[i]
const cellFg = RGBA.fromValues(fg[i * 4], fg[i * 4 + 1], fg[i * 4 + 2], fg[i * 4 + 3])
const cellBg = RGBA.fromValues(bg[i * 4], bg[i * 4 + 1], bg[i * 4 + 2], bg[i * 4 + 3])
const cellFg = RGBA.fromValues(fg[i * 5], fg[i * 5 + 1], fg[i * 5 + 2], fg[i * 5 + 3], fg[i * 5 + 4])
const cellBg = RGBA.fromValues(bg[i * 5], bg[i * 5 + 1], bg[i * 5 + 2], bg[i * 5 + 3], bg[i * 5 + 4])
const cellAttrs = attributes[i] & 0xff

// Continuation cells are placeholders for wide characters (emojis, CJK)
Expand Down
197 changes: 197 additions & 0 deletions packages/core/src/examples/terminal_ansi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
#!/usr/bin/env bun

import {
CliRenderer,
createCliRenderer,
RGBA,
TextAttributes,
TextRenderable,
FrameBufferRenderable,
BoxRenderable,
} from "../index"
import { ScrollBoxRenderable } from "../renderables/ScrollBox"
import { setupCommonDemoKeys } from "./lib/standalone-keys"

/**
* This demo showcases 256 indexed ANSI colors using RGBA.fromIndex().
* It renders the full 0-255 palette as colored blocks in a grid.
*/

let scrollBox: ScrollBoxRenderable | null = null

export function run(renderer: CliRenderer): void {
renderer.start()
renderer.setBackgroundColor(RGBA.fromIndex(0)) // Use indexed black

const mainContainer = new BoxRenderable(renderer, {
id: "main-container",
flexGrow: 1,
flexDirection: "column",
})
renderer.root.add(mainContainer)

scrollBox = new ScrollBoxRenderable(renderer, {
id: "ansi-scroll-box",
stickyScroll: false,
border: true,
borderColor: RGBA.fromIndex(1),
title: "256 ANSI Indexed Colors (Ctrl+C to exit)",
titleAlignment: "center",
contentOptions: {
paddingLeft: 2,
paddingRight: 2,
paddingTop: 1,
},
})
mainContainer.add(scrollBox)

const contentContainer = new BoxRenderable(renderer, {
id: "ansi-content",
width: "auto",
flexDirection: "column",
})
scrollBox.add(contentContainer)

// --- Standard colors (0-7) ---
const standardLabel = new TextRenderable(renderer, {
id: "standard-label",
content: "Standard Colors (0-7)",
fg: RGBA.fromIndex(15),
})
contentContainer.add(standardLabel)

const standardBuffer = new FrameBufferRenderable(renderer, {
id: "standard-buffer",
width: 40,
height: 2,
marginTop: 1,
})
contentContainer.add(standardBuffer)
drawColorRow(standardBuffer.frameBuffer, 0, 8, 40)

// --- Bright colors (8-15) ---
const brightLabel = new TextRenderable(renderer, {
id: "bright-label",
content: "Bright Colors (8-15)",
fg: RGBA.fromIndex(15),
marginTop: 1,
})
contentContainer.add(brightLabel)

const brightBuffer = new FrameBufferRenderable(renderer, {
id: "bright-buffer",
width: 40,
height: 2,
marginTop: 1,
})
contentContainer.add(brightBuffer)
drawColorRow(brightBuffer.frameBuffer, 8, 16, 40)

// --- 6x6x6 Color Cube (16-231) ---
const cubeLabel = new TextRenderable(renderer, {
id: "cube-label",
content: "6x6x6 Color Cube (16-231)",
fg: RGBA.fromIndex(15),
marginTop: 1,
})
contentContainer.add(cubeLabel)

const cubeBuffer = new FrameBufferRenderable(renderer, {
id: "cube-buffer",
width: 72,
height: 12,
marginTop: 1,
})
contentContainer.add(cubeBuffer)
drawColorCube(cubeBuffer.frameBuffer)

// --- Grayscale Ramp (232-255) ---
const grayLabel = new TextRenderable(renderer, {
id: "gray-label",
content: "Grayscale Ramp (232-255)",
fg: RGBA.fromIndex(15),
marginTop: 1,
})
contentContainer.add(grayLabel)

const grayBuffer = new FrameBufferRenderable(renderer, {
id: "gray-buffer",
width: 72,
height: 2,
marginTop: 1,
})
contentContainer.add(grayBuffer)
drawColorRow(grayBuffer.frameBuffer, 232, 256, 72)

// --- Info text ---
const infoText = new TextRenderable(renderer, {
id: "info-text",
content: "All colors rendered using RGBA.fromIndex(n) — indexed color meta is packed into RGBA[4]",
fg: RGBA.fromIndex(244),
marginTop: 2,
})
contentContainer.add(infoText)
}

function drawColorRow(
buffer: FrameBufferRenderable["frameBuffer"],
startIndex: number,
endIndex: number,
width: number,
): void {
const count = endIndex - startIndex
const cellWidth = Math.floor(width / count)

for (let i = 0; i < count; i++) {
const colorIndex = startIndex + i
const bg = RGBA.fromIndex(colorIndex)
const fg = colorIndex < 8 || (colorIndex >= 232 && colorIndex < 244)
? RGBA.fromIndex(15)
: RGBA.fromIndex(0)
const label = colorIndex.toString().padStart(3, " ")

for (let dy = 0; dy < 2; dy++) {
for (let dx = 0; dx < cellWidth; dx++) {
const x = i * cellWidth + dx
if (dy === 0 && dx < label.length) {
buffer.drawText(label[dx], x, dy, fg, bg, TextAttributes.NONE)
} else {
buffer.setCell(x, dy, " ", fg, bg)
}
}
}
}
}

function drawColorCube(buffer: FrameBufferRenderable["frameBuffer"]): void {
// 6x6x6 cube: 6 rows of 36 colors, each cell 2 chars wide
for (let row = 0; row < 6; row++) {
for (let col = 0; col < 36; col++) {
const colorIndex = 16 + row * 36 + col
const bg = RGBA.fromIndex(colorIndex)
const x = col * 2
const y = row * 2

for (let dy = 0; dy < 2; dy++) {
for (let dx = 0; dx < 2; dx++) {
buffer.setCell(x + dx, y + dy, " ", RGBA.fromIndex(0), bg)
}
}
}
}
}

export function destroy(renderer: CliRenderer): void {
if (scrollBox) {
renderer.root.remove("main-container")
scrollBox = null
}
}

if (import.meta.main) {
const renderer = await createCliRenderer({
exitOnCtrlC: true,
})
run(renderer)
setupCommonDemoKeys(renderer)
}
11 changes: 9 additions & 2 deletions packages/core/src/lib/RGBA.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,18 @@ describe("RGBA class", () => {
expect(rgba.a).toBeCloseTo(0.4, 5)
})

test("uses same buffer reference", () => {
const array = new Float32Array([0.1, 0.2, 0.3, 0.4])
test("uses same buffer reference for 5-element array", () => {
const array = new Float32Array([0.1, 0.2, 0.3, 0.4, 0.0])
const rgba = RGBA.fromArray(array)
expect(rgba.buffer).toBe(array)
})

test("creates new buffer for legacy 4-element array", () => {
const array = new Float32Array([0.1, 0.2, 0.3, 0.4])
const rgba = RGBA.fromArray(array)
expect(rgba.buffer.length).toBe(5)
expect(rgba.buffer[4]).toBe(0)
})
})

describe("fromValues", () => {
Expand Down
78 changes: 74 additions & 4 deletions packages/core/src/lib/RGBA.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,57 @@ export class RGBA {
}

static fromArray(array: Float32Array) {
if (array.length === 4) {
// Legacy 4-element array — add meta = 0 (RGB)
const buf = new Float32Array(5)
buf.set(array)
return new RGBA(buf)
}
return new RGBA(array)
}

static fromValues(r: number, g: number, b: number, a: number = 1.0) {
return new RGBA(new Float32Array([r, g, b, a]))
static fromValues(r: number, g: number, b: number, a: number = 1.0, meta: number = 0) {
return new RGBA(new Float32Array([r, g, b, a, meta]))
}

static fromInts(r: number, g: number, b: number, a: number = 255) {
return new RGBA(new Float32Array([r / 255, g / 255, b / 255, a / 255]))
return new RGBA(new Float32Array([r / 255, g / 255, b / 255, a / 255, 0]))
}

static fromIndex(index: number): RGBA {
// Approximate RGB from 256-color palette, with indexed meta
const meta = 256 + index // colorType=1 (indexed) * 256 + index
if (index < 16) {
// Standard + bright colors - use predefined values
const STANDARD: [number, number, number][] = [
[0, 0, 0], [0.5, 0, 0], [0, 0.5, 0], [0.5, 0.5, 0],
[0, 0, 0.5], [0.5, 0, 0.5], [0, 0.5, 0.5], [0.75, 0.75, 0.75],
[0.5, 0.5, 0.5], [1, 0, 0], [0, 1, 0], [1, 1, 0],
[0, 0, 1], [1, 0, 1], [0, 1, 1], [1, 1, 1],
]
const [r, g, b] = STANDARD[index]
return new RGBA(new Float32Array([r, g, b, 1.0, meta]))
} else if (index < 232) {
// 6x6x6 color cube (indices 16-231)
const ci = index - 16
const ri = Math.floor(ci / 36)
const gi = Math.floor((ci % 36) / 6)
const bi = ci % 6
return new RGBA(new Float32Array([
ri === 0 ? 0 : (55 + ri * 40) / 255,
gi === 0 ? 0 : (55 + gi * 40) / 255,
bi === 0 ? 0 : (55 + bi * 40) / 255,
1.0, meta,
]))
} else {
// Grayscale (indices 232-255)
const gray = (8 + (index - 232) * 10) / 255
return new RGBA(new Float32Array([gray, gray, gray, 1.0, meta]))
}
}

static defaultColor(): RGBA {
return new RGBA(new Float32Array([1.0, 1.0, 1.0, 1.0, 512])) // colorType=2 (default) * 256
}

static fromHex(hex: string): RGBA {
Expand Down Expand Up @@ -57,6 +99,34 @@ export class RGBA {
this.buffer[3] = value
}

get meta(): number {
return this.buffer[4]
}

set meta(value: number) {
this.buffer[4] = value
}

get colorType(): number {
return Math.floor(this.buffer[4] / 256)
}

get colorIndex(): number {
return this.buffer[4] % 256
}

isRgb(): boolean {
return this.colorType === 0
}

isIndexed(): boolean {
return this.colorType === 1
}

isDefault(): boolean {
return this.colorType === 2
}

map<R>(fn: (value: number) => R) {
return [fn(this.r), fn(this.g), fn(this.b), fn(this.a)]
}
Expand All @@ -67,7 +137,7 @@ export class RGBA {

equals(other?: RGBA): boolean {
if (!other) return false
return this.r === other.r && this.g === other.g && this.b === other.b && this.a === other.a
return this.r === other.r && this.g === other.g && this.b === other.b && this.a === other.a && this.meta === other.meta
}
}

Expand Down
Loading