Files
oh-my-pi/packages/tui/src/components/box.ts
T
can1357 7222bc1eae feat(tui): removed cursor positioning and refactored TUI rendering with explicit env defaults
- Removed getCursorPosition() method from Component interface and implementations, eliminating hardware cursor positioning support.
- Changed PI_HARDWARE_CURSOR environment variable default from implicit true to explicit '1' with fallback pattern.
- Changed PI_CLEAR_ON_SHRINK environment variable default from implicit false to explicit '0' with fallback pattern.
- Added screen clearing on TUI startup to prevent shell prompts from bleeding into first rendered frame.
- Refactored full-render logic into reusable fullRender() helper function.
2026-02-06 05:18:02 +01:00

135 lines
3.0 KiB
TypeScript

import type { Component } from "../tui";
import { applyBackgroundToLine, padding, visibleWidth } from "../utils";
type Cache = {
key: bigint;
result: string[];
};
/**
* Box component - a container that applies padding and background to all children
*/
export class Box implements Component {
children: Component[] = [];
private paddingX: number;
private paddingY: number;
private bgFn?: (text: string) => string;
// Cache for rendered output
private cached?: Cache;
constructor(paddingX = 1, paddingY = 1, bgFn?: (text: string) => string) {
this.paddingX = paddingX;
this.paddingY = paddingY;
this.bgFn = bgFn;
}
addChild(component: Component): void {
this.children.push(component);
this.invalidateCache();
}
removeChild(component: Component): void {
const index = this.children.indexOf(component);
if (index !== -1) {
this.children.splice(index, 1);
this.invalidateCache();
}
}
clear(): void {
this.children = [];
this.invalidateCache();
}
setBgFn(bgFn?: (text: string) => string): void {
this.bgFn = bgFn;
// Don't invalidate here - we'll detect bgFn changes by sampling output
}
private invalidateCache(): void {
this.cached = undefined;
}
private computeCacheKey(width: number, childLines: string[], bgSample: string | undefined): bigint {
return Bun.hash.xxHash64(JSON.stringify([width, bgSample, childLines]));
}
private matchCache(cacheKey: bigint): boolean {
return this.cached?.key === cacheKey;
}
invalidate(): void {
this.invalidateCache();
for (const child of this.children) {
child.invalidate?.();
}
}
render(width: number): string[] {
if (this.children.length === 0) {
return [];
}
const contentWidth = Math.max(1, width - this.paddingX * 2);
const leftPad = padding(this.paddingX);
// Render all children
const childLines: string[] = [];
for (const child of this.children) {
const lines = child.render(contentWidth);
for (const line of lines) {
childLines.push(leftPad + line);
}
}
if (childLines.length === 0) {
return [];
}
// Check if bgFn output changed by sampling
const bgSample = this.bgFn ? this.bgFn("test") : undefined;
const cacheKey = this.computeCacheKey(width, childLines, bgSample);
// Check cache validity
if (this.matchCache(cacheKey)) {
return this.cached!.result;
}
// Apply background and padding
const result: string[] = [];
// Top padding
for (let i = 0; i < this.paddingY; i++) {
result.push(this.applyBg("", width));
}
// Content
for (const line of childLines) {
result.push(this.applyBg(line, width));
}
// Bottom padding
for (let i = 0; i < this.paddingY; i++) {
result.push(this.applyBg("", width));
}
// Update cache
this.cached = { key: cacheKey, result };
return result;
}
private applyBg(line: string, width: number): string {
const visLen = visibleWidth(line);
const padNeeded = Math.max(0, width - visLen);
const padded = line + padding(padNeeded);
if (this.bgFn) {
return applyBackgroundToLine(padded, width, this.bgFn);
}
return padded;
}
}