mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-19 06:01:14 +00:00
- Add isImageLine() to terminal-image.ts with single startsWith check based on detected terminal protocol - Replace dual includes() checks in tui.ts with imported isImageLine() - Add image line handling to markdown.ts to skip wrapping and margins for image escapes - Consolidate Box cache into RenderCache type with childLines/width/bgSample/lines fields - Use in-place mutation in applyLineResets() to avoid array allocation
137 lines
3.1 KiB
TypeScript
137 lines
3.1 KiB
TypeScript
import type { Component } from "../tui.js";
|
|
import { applyBackgroundToLine, visibleWidth } from "../utils.js";
|
|
|
|
type RenderCache = {
|
|
childLines: string[];
|
|
width: number;
|
|
bgSample: string | undefined;
|
|
lines: 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 cache?: RenderCache;
|
|
|
|
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.cache = undefined;
|
|
}
|
|
|
|
private matchCache(width: number, childLines: string[], bgSample: string | undefined): boolean {
|
|
const cache = this.cache;
|
|
return (
|
|
!!cache &&
|
|
cache.width === width &&
|
|
cache.bgSample === bgSample &&
|
|
cache.childLines.length === childLines.length &&
|
|
cache.childLines.every((line, i) => line === childLines[i])
|
|
);
|
|
}
|
|
|
|
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 = " ".repeat(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;
|
|
|
|
// Check cache validity
|
|
if (this.matchCache(width, childLines, bgSample)) {
|
|
return this.cache!.lines;
|
|
}
|
|
|
|
// 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.cache = { childLines, width, bgSample, lines: result };
|
|
|
|
return result;
|
|
}
|
|
|
|
private applyBg(line: string, width: number): string {
|
|
const visLen = visibleWidth(line);
|
|
const padNeeded = Math.max(0, width - visLen);
|
|
const padded = line + " ".repeat(padNeeded);
|
|
|
|
if (this.bgFn) {
|
|
return applyBackgroundToLine(padded, width, this.bgFn);
|
|
}
|
|
return padded;
|
|
}
|
|
}
|