refactor: extracted padding logic into reusable utility function with optimized buffer allocation

- Extracted manual space padding logic into a reusable `padding()` utility function across all UI components and utilities.
- Optimized padding operations by introducing a pre-allocated 512-space buffer in the `padding()` function to reduce repeated string allocations.
- Updated all imports across 30 files to use the new `padding()` function from pi-tui instead of inline `' '.repeat()` calls.
- Renamed local variables from `padding` to `pad`, `padSize`, `indent`, or `linePad` to avoid naming conflicts with the imported `padding()` function.
- Exported the `padding()` utility function from the tui package index for public use.
This commit is contained in:
can1357
2026-01-31 13:36:54 +01:00
parent 9a22622f9c
commit cc90ed37f8
30 changed files with 121 additions and 92 deletions
+2
View File
@@ -1,8 +1,10 @@
# Changelog
## [Unreleased]
### Changed
- Replaced manual space padding with `padding()` utility function from pi-tui across UI components for consistent whitespace handling
- Improved rendering performance for Python cell output by implementing caching in the table and cell results renderers
- Updated task tool documentation to clarify that subagents can access parent conversation context via a searchable file, reducing need to repeat information in context parameter
- Updated plan mode prompt to guide model toward using `edit` tool for incremental plan updates instead of defaulting to `write`
@@ -2,7 +2,7 @@
* Snake game hook - play snake with /snake command
*/
import type { HookAPI } from "@oh-my-pi/pi-coding-agent";
import { matchesKey, visibleWidth } from "@oh-my-pi/pi-tui";
import { matchesKey, padding, visibleWidth } from "@oh-my-pi/pi-tui";
const GAME_WIDTH = 40;
const GAME_HEIGHT = 15;
@@ -227,8 +227,8 @@ class SnakeComponent {
// Helper to pad content inside box
const boxLine = (content: string) => {
const contentLen = visibleWidth(content);
const padding = Math.max(0, boxWidth - contentLen);
return dim(" │") + content + " ".repeat(padding) + dim("│");
const pad = Math.max(0, boxWidth - contentLen);
return dim(" │") + content + padding(pad) + dim("│");
};
// Top border
@@ -291,8 +291,8 @@ class SnakeComponent {
private padLine(line: string, width: number): string {
// Calculate visible length (strip ANSI codes)
const visibleLen = line.replace(/\x1b\[[0-9;]*m/g, "").length;
const padding = Math.max(0, width - visibleLen);
return line + " ".repeat(padding);
const pad = Math.max(0, width - visibleLen);
return line + padding(pad);
}
dispose(): void {
@@ -1,7 +1,7 @@
/**
* Armin says hi! A fun easter egg with animated XBM art.
*/
import type { Component, TUI } from "@oh-my-pi/pi-tui";
import { type Component, padding, type TUI } from "@oh-my-pi/pi-tui";
import { theme } from "../../modes/theme/theme";
// XBM image: 31x36 pixels, LSB first, 1=background, 0=foreground
@@ -87,20 +87,20 @@ export class ArminComponent implements Component {
return this.cachedLines;
}
const padding = 1;
const availableWidth = width - padding;
const indent = 1;
const availableWidth = width - indent;
this.cachedLines = this.currentGrid.map(row => {
// Clip row to available width before applying color
const clipped = row.slice(0, availableWidth).join("");
const padRight = Math.max(0, width - padding - clipped.length);
return ` ${theme.fg("accent", clipped)}${" ".repeat(padRight)}`;
const padRight = Math.max(0, width - indent - clipped.length);
return ` ${theme.fg("accent", clipped)}${padding(padRight)}`;
});
// Add "ARMIN SAYS HI" at the end
const message = "ARMIN SAYS HI";
const msgPadRight = Math.max(0, width - padding - message.length);
this.cachedLines.push(` ${theme.fg("accent", message)}${" ".repeat(msgPadRight)}`);
const msgPadRight = Math.max(0, width - indent - message.length);
this.cachedLines.push(` ${theme.fg("accent", message)}${padding(msgPadRight)}`);
this.cachedWidth = width;
this.cachedVersion = this.gridVersion;
@@ -11,7 +11,16 @@
* - Space: Toggle selected item (or master switch)
* - Esc: Close dashboard (clears search first if active)
*/
import { type Component, Container, matchesKey, Spacer, Text, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
import {
type Component,
Container,
matchesKey,
padding,
Spacer,
Text,
truncateToWidth,
visibleWidth,
} from "@oh-my-pi/pi-tui";
import type { SettingsManager } from "../../../config/settings-manager";
import { DynamicBorder } from "../../../modes/components/dynamic-border";
import { theme } from "../../../modes/theme/theme";
@@ -296,7 +305,7 @@ class TwoColumnBody implements Component {
for (let i = 0; i < numLines; i++) {
const left = truncateToWidth(leftLines[i] ?? "", leftWidth);
const leftPadded = left + " ".repeat(Math.max(0, leftWidth - visibleWidth(left)));
const leftPadded = left + padding(Math.max(0, leftWidth - visibleWidth(left)));
const right = truncateToWidth(rightLines[i] ?? "", rightWidth);
combined.push(leftPadded + separator + right);
}
@@ -5,7 +5,7 @@
* that toggles the entire provider. All items below are dimmed when the
* master switch is off.
*/
import { type Component, matchesKey, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
import { type Component, matchesKey, padding, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
import { isProviderEnabled } from "../../../discovery";
import { theme } from "../../../modes/theme/theme";
import { applyFilter } from "./state-manager";
@@ -272,7 +272,7 @@ export class ExtensionList implements Component {
if (width >= targetWidth) {
return truncateToWidth(text, targetWidth);
}
return text + " ".repeat(targetWidth - width);
return text + padding(targetWidth - width);
}
private rebuildList(): void {
@@ -1,7 +1,7 @@
import * as fs from "node:fs";
import * as path from "node:path";
import type { AssistantMessage } from "@oh-my-pi/pi-ai";
import { type Component, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
import { type Component, padding, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
import { isEnoent } from "@oh-my-pi/pi-utils";
import { theme } from "../../modes/theme/theme";
import type { AgentSession } from "../../session/agent-session";
@@ -283,8 +283,8 @@ export class FooterComponent implements Component {
let statsLine: string;
if (totalNeeded <= width) {
// Both fit - add padding to right-align model
const padding = " ".repeat(width - statsLeftWidth - rightSideWidth);
statsLine = statsLeft + padding + rightSide;
const pad = padding(width - statsLeftWidth - rightSideWidth);
statsLine = statsLeft + pad + rightSide;
} else {
// Need to truncate right side
const availableForRight = width - statsLeftWidth - minPadding;
@@ -293,8 +293,8 @@ export class FooterComponent implements Component {
const plainRightSide = rightSide.replace(/\x1b\[[0-9;]*m/g, "");
const truncatedPlain = plainRightSide.substring(0, availableForRight);
// For simplicity, just use plain truncated version (loses color, but fits)
const padding = " ".repeat(width - statsLeftWidth - truncatedPlain.length);
statsLine = statsLeft + padding + truncatedPlain;
const pad = padding(width - statsLeftWidth - truncatedPlain.length);
statsLine = statsLeft + pad + truncatedPlain;
} else {
// Not enough space for right side at all
statsLine = statsLeft;
@@ -3,6 +3,7 @@ import {
Container,
Input,
matchesKey,
padding,
Spacer,
Text,
truncateToWidth,
@@ -50,7 +51,7 @@ class HistoryResultsList implements Component {
const cursorSymbol = `${theme.nav.cursor} `;
const cursorWidth = visibleWidth(cursorSymbol);
const cursor = isSelected ? theme.fg("accent", cursorSymbol) : " ".repeat(cursorWidth);
const cursor = isSelected ? theme.fg("accent", cursorSymbol) : padding(cursorWidth);
const maxWidth = width - cursorWidth;
const normalized = entry.prompt.replace(/\s+/g, " ").trim();
@@ -2,7 +2,7 @@
* Generic selector component for hooks.
* Displays a list of string options with keyboard navigation.
*/
import { Container, matchesKey, Spacer, Text, type TUI, visibleWidth } from "@oh-my-pi/pi-tui";
import { Container, matchesKey, padding, Spacer, Text, type TUI, visibleWidth } from "@oh-my-pi/pi-tui";
import { theme } from "../../modes/theme/theme";
import { CountdownTimer } from "./countdown-timer";
import { DynamicBorder } from "./dynamic-border";
@@ -29,7 +29,7 @@ class OutlinedList extends Container {
const innerWidth = Math.max(1, width - 2);
const content = this.lines.map(line => {
const pad = Math.max(0, innerWidth - visibleWidth(line));
return `${borderColor(theme.boxSharp.vertical)}${line}${" ".repeat(pad)}${borderColor(theme.boxSharp.vertical)}`;
return `${borderColor(theme.boxSharp.vertical)}${line}${padding(pad)}${borderColor(theme.boxSharp.vertical)}`;
});
return [horizontal, ...content, horizontal];
}
@@ -3,6 +3,7 @@ import {
Container,
Input,
matchesKey,
padding,
Spacer,
Text,
truncateToWidth,
@@ -122,7 +123,7 @@ class SessionList implements Component {
// First line: cursor + title (or first message if no title)
const cursorSymbol = `${theme.nav.cursor} `;
const cursorWidth = visibleWidth(cursorSymbol);
const cursor = isSelected ? theme.fg("accent", cursorSymbol) : " ".repeat(cursorWidth);
const cursor = isSelected ? theme.fg("accent", cursorSymbol) : padding(cursorWidth);
const maxWidth = width - cursorWidth; // Account for cursor width
if (session.title) {
@@ -8,7 +8,7 @@
* - Shift+J/K: Reorder segment within column
* - Live preview shown in the actual status line above
*/
import { Container, matchesKey } from "@oh-my-pi/pi-tui";
import { Container, matchesKey, padding } from "@oh-my-pi/pi-tui";
import type { StatusLineSegmentId } from "../../config/settings-manager";
import { theme } from "../../modes/theme/theme";
import { ALL_SEGMENT_IDS } from "./status-line/segments";
@@ -351,7 +351,7 @@ export class StatusLineSegmentEditorComponent extends Container {
}
// Pad to column width (accounting for ANSI codes)
const padding = colWidth - label.length - 1;
return text + " ".repeat(Math.max(0, padding));
const padSize = colWidth - label.length - 1;
return text + padding(Math.max(0, padSize));
}
}
@@ -1,7 +1,7 @@
import * as fs from "node:fs";
import * as path from "node:path";
import type { AssistantMessage } from "@oh-my-pi/pi-ai";
import { type Component, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
import { type Component, padding, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
import { $ } from "bun";
import type { StatusLineSegmentOptions, StatusLineSettings } from "../../config/settings-manager";
import { theme } from "../../modes/theme/theme";
@@ -378,7 +378,7 @@ export class StatusLineComponent implements Component {
leftWidth = groupWidth(left, leftCapWidth, leftSepWidth);
rightWidth = groupWidth(right, rightCapWidth, rightSepWidth);
const gapWidth = Math.max(1, topFillWidth - leftWidth - rightWidth);
return leftGroup + " ".repeat(gapWidth) + rightGroup;
return leftGroup + padding(gapWidth) + rightGroup;
}
getTopBorder(width: number): { content: string; width: number } {
@@ -1,4 +1,4 @@
import { type Component, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
import { type Component, padding, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
import { APP_NAME } from "../../config";
import { theme } from "../../modes/theme/theme";
@@ -169,7 +169,7 @@ export class WelcomeComponent implements Component {
}
const leftPad = Math.floor((width - visLen) / 2);
const rightPad = width - visLen - leftPad;
return " ".repeat(leftPad) + text + " ".repeat(rightPad);
return padding(leftPad) + text + padding(rightPad);
}
/** Apply magenta→cyan gradient to a string */
@@ -224,6 +224,6 @@ export class WelcomeComponent implements Component {
}
return `${truncated}${ellipsis}`;
}
return str + " ".repeat(width - visLen);
return str + padding(width - visLen);
}
}
@@ -2,7 +2,7 @@ import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import type { UsageLimit, UsageReport } from "@oh-my-pi/pi-ai";
import { Loader, Markdown, Spacer, Text, visibleWidth } from "@oh-my-pi/pi-tui";
import { Loader, Markdown, padding, Spacer, Text, visibleWidth } from "@oh-my-pi/pi-tui";
import { $ } from "bun";
import { nanoid } from "nanoid";
import { loadCustomShare } from "../../export/custom-share";
@@ -773,7 +773,7 @@ function formatAccountHeader(limit: UsageLimit, report: UsageReport, index: numb
function padColumn(text: string, width: number): string {
const visible = visibleWidth(text);
if (visible >= width) return text;
return `${text}${" ".repeat(width - visible)}`;
return `${text}${padding(width - visible)}`;
}
function resolveAggregateStatus(limits: UsageLimit[]): UsageLimit["status"] {
+3 -1
View File
@@ -4,6 +4,8 @@
* Handles line endings, BOM, whitespace, and Unicode normalization.
*/
import { padding } from "@oh-my-pi/pi-tui";
// ═══════════════════════════════════════════════════════════════════════════
// Line Ending Utilities
// ═══════════════════════════════════════════════════════════════════════════
@@ -194,7 +196,7 @@ export function convertLeadingTabsToSpaces(text: string, spacesPerTab: number):
if (trimmed.length === 0) return line;
const leading = getLeadingWhitespace(line);
if (!leading.includes("\t") || leading.includes(" ")) return line;
const converted = " ".repeat(leading.length * spacesPerTab);
const converted = padding(leading.length * spacesPerTab);
return converted + trimmed;
})
.join("\n");
@@ -1,7 +1,7 @@
/**
* Bordered output container with optional header and sections.
*/
import { visibleWidth } from "@oh-my-pi/pi-tui";
import { padding, visibleWidth } from "@oh-my-pi/pi-tui";
import type { Theme } from "../modes/theme/theme";
import type { State } from "./types";
import { getStateBgColor, padToWidth, truncateToWidth } from "./utils";
@@ -70,7 +70,7 @@ export function renderOutputBlock(options: OutputBlockOptions, theme: Theme): st
const allLines = section.lines.flatMap(l => l.split("\n"));
for (const line of allLines) {
const text = truncateToWidth(line, contentWidth, theme.format.ellipsis);
const innerPadding = " ".repeat(Math.max(0, contentWidth - visibleWidth(text)));
const innerPadding = padding(Math.max(0, contentWidth - visibleWidth(text)));
const fullLine = `${contentPrefix}${text}${innerPadding}${contentSuffix}`;
lines.push(padToWidth(fullLine, lineWidth, bgFn));
}
+2 -2
View File
@@ -1,7 +1,7 @@
/**
* Shared helpers for tool-rendered UI components.
*/
import { truncateToWidth as truncateToWidthBase, visibleWidth } from "@oh-my-pi/pi-tui";
import { padding, truncateToWidth as truncateToWidthBase, visibleWidth } from "@oh-my-pi/pi-tui";
import type { Theme, ThemeBg } from "../modes/theme/theme";
import type { IconType, State } from "./types";
@@ -24,7 +24,7 @@ export function truncateToWidth(text: string, width: number, ellipsis: string):
export function padToWidth(text: string, width: number, bgFn?: (s: string) => string): string {
if (width <= 0) return bgFn ? bgFn(text) : text;
const paddingNeeded = Math.max(0, width - visibleWidth(text));
const padded = paddingNeeded > 0 ? text + " ".repeat(paddingNeeded) : text;
const padded = paddingNeeded > 0 ? text + padding(paddingNeeded) : text;
return bgFn ? bgFn(padded) : padded;
}
+4 -3
View File
@@ -12,6 +12,7 @@ import * as fs from "node:fs";
import { join } from "node:path";
import { parseArgs } from "node:util";
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
import { padding } from "@oh-my-pi/pi-tui";
import { TempDir } from "@oh-my-pi/pi-utils";
import { generateJsonReport, generateReport } from "./report";
import { type BenchmarkConfig, type ProgressEvent, runBenchmark } from "./runner";
@@ -460,8 +461,8 @@ class LiveProgress {
}
private writeLine(line: string): void {
const padding = this.lastLineLength > line.length ? " ".repeat(this.lastLineLength - line.length) : "";
process.stdout.write(`\r${line}${padding}`);
const pad = this.lastLineLength > line.length ? padding(this.lastLineLength - line.length) : "";
process.stdout.write(`\r${line}${pad}`);
this.lastLineLength = line.length;
}
@@ -470,7 +471,7 @@ class LiveProgress {
return;
}
if (this.lastLineLength > 0) {
process.stdout.write("\r" + " ".repeat(this.lastLineLength) + "\r");
process.stdout.write("\r" + padding(this.lastLineLength) + "\r");
this.lastLineLength = 0;
}
}
+3 -1
View File
@@ -1,3 +1,5 @@
import { padding } from "@oh-my-pi/pi-tui";
/**
* Code mutations for edit benchmark generation.
*
@@ -84,7 +86,7 @@ function applyCandidate(lines: string[], candidate: Candidate): MutationInfo {
function stripStrings(line: string): string {
const pattern = /(?<quote>['"])(?<body>(?:\\.|[^\\\n])*?)\k<quote>/g;
return line.replace(pattern, (match) => " ".repeat(match.length));
return line.replace(pattern, (match) => padding(match.length));
}
function mutateIdentifier(identifier: string): string | null {
+7
View File
@@ -1,6 +1,13 @@
# Changelog
## [Unreleased]
### Added
- Exported `padding` utility function for creating space-padded strings efficiently
### Changed
- Optimized padding operations across all components to use pre-allocated space buffer for better performance
## [9.2.2] - 2026-01-31
+3 -3
View File
@@ -1,5 +1,5 @@
import type { Component } from "../tui";
import { applyBackgroundToLine, visibleWidth } from "../utils";
import { applyBackgroundToLine, padding, visibleWidth } from "../utils";
type Cache = {
key: string[];
@@ -77,7 +77,7 @@ export class Box implements Component {
}
const contentWidth = Math.max(1, width - this.paddingX * 2);
const leftPad = " ".repeat(this.paddingX);
const leftPad = padding(this.paddingX);
// Render all children
const childLines: string[] = [];
@@ -127,7 +127,7 @@ export class Box implements Component {
private applyBg(line: string, width: number): string {
const visLen = visibleWidth(line);
const padNeeded = Math.max(0, width - visLen);
const padded = line + " ".repeat(padNeeded);
const padded = line + padding(padNeeded);
if (this.bgFn) {
return applyBackgroundToLine(padded, width, this.bgFn);
+8 -8
View File
@@ -2,7 +2,7 @@ import type { AutocompleteProvider, CombinedAutocompleteProvider } from "../auto
import { matchesKey } from "../keys";
import type { SymbolTheme } from "../symbols";
import { type Component, CURSOR_MARKER, type Focusable } from "../tui";
import { getSegmenter, isPunctuationChar, isWhitespaceChar, truncateToWidth, visibleWidth } from "../utils";
import { getSegmenter, isPunctuationChar, isWhitespaceChar, padding, truncateToWidth, visibleWidth } from "../utils";
import { SelectList, type SelectListTheme } from "./select-list";
const segmenter = getSegmenter();
@@ -491,7 +491,7 @@ export class Editor implements Component, Focusable {
const borderWidth = paddingX + 1;
const topLeft = this.borderColor(`${box.topLeft}${box.horizontal.repeat(paddingX)}`);
const topRight = this.borderColor(`${box.horizontal.repeat(paddingX)}${box.topRight}`);
const bottomLeft = this.borderColor(`${box.bottomLeft}${box.horizontal}${" ".repeat(Math.max(0, paddingX - 1))}`);
const bottomLeft = this.borderColor(`${box.bottomLeft}${box.horizontal}${padding(Math.max(0, paddingX - 1))}`);
const horizontal = this.borderColor(box.horizontal);
// Layout the text
@@ -568,19 +568,19 @@ export class Editor implements Component, Focusable {
// All lines have consistent borders based on padding
const isLastLine = layoutLine === visibleLayoutLines[visibleLayoutLines.length - 1];
const padding = " ".repeat(Math.max(0, lineContentWidth - displayWidth));
const linePad = padding(Math.max(0, lineContentWidth - displayWidth));
const rightPaddingWidth = Math.max(0, paddingX - (cursorInPadding ? 1 : 0));
if (isLastLine) {
const bottomRightPadding = Math.max(0, paddingX - 1 - (cursorInPadding ? 1 : 0));
const bottomRightAdjusted = this.borderColor(
`${" ".repeat(bottomRightPadding)}${box.horizontal}${box.bottomRight}`,
`${padding(bottomRightPadding)}${box.horizontal}${box.bottomRight}`,
);
result.push(`${bottomLeft}${displayText}${padding}${bottomRightAdjusted}`);
result.push(`${bottomLeft}${displayText}${linePad}${bottomRightAdjusted}`);
} else {
const leftBorder = this.borderColor(`${box.vertical}${" ".repeat(paddingX)}`);
const rightBorder = this.borderColor(`${" ".repeat(rightPaddingWidth)}${box.vertical}`);
result.push(leftBorder + displayText + padding + rightBorder);
const leftBorder = this.borderColor(`${box.vertical}${padding(paddingX)}`);
const rightBorder = this.borderColor(`${padding(rightPaddingWidth)}${box.vertical}`);
result.push(leftBorder + displayText + linePad + rightBorder);
}
}
+3 -3
View File
@@ -1,6 +1,6 @@
import { getEditorKeybindings } from "../keybindings";
import { type Component, CURSOR_MARKER, type Focusable } from "../tui";
import { getSegmenter, isPunctuationChar, isWhitespaceChar, visibleWidth } from "../utils";
import { getSegmenter, isPunctuationChar, isWhitespaceChar, padding, visibleWidth } from "../utils";
const segmenter = getSegmenter();
@@ -338,8 +338,8 @@ export class Input implements Component, Focusable {
// Calculate visual width
const visualLength = visibleWidth(textWithCursor);
const padding = " ".repeat(Math.max(0, availableWidth - visualLength));
const line = prompt + textWithCursor + padding;
const pad = padding(Math.max(0, availableWidth - visualLength));
const line = prompt + textWithCursor + pad;
return [line];
}
+8 -8
View File
@@ -3,7 +3,7 @@ import type { MermaidImage } from "../mermaid";
import type { SymbolTheme } from "../symbols";
import { encodeITerm2, encodeKitty, getCellDimensions, ImageProtocol, TERMINAL_INFO } from "../terminal-image";
import type { Component } from "../tui";
import { applyBackgroundToLine, visibleWidth, wrapTextWithAnsi } from "../utils";
import { applyBackgroundToLine, padding, visibleWidth, wrapTextWithAnsi } from "../utils";
/**
* Default text styling for markdown content.
@@ -151,8 +151,8 @@ export class Markdown implements Component {
}
// Add margins and background to each wrapped line
const leftMargin = " ".repeat(this.paddingX);
const rightMargin = " ".repeat(this.paddingX);
const leftMargin = padding(this.paddingX);
const rightMargin = padding(this.paddingX);
const bgFn = this.defaultTextStyle?.bgColor;
const contentLines: string[] = [];
@@ -174,7 +174,7 @@ export class Markdown implements Component {
}
// Add top/bottom padding (empty lines)
const emptyLine = " ".repeat(width);
const emptyLine = padding(width);
const emptyLines: string[] = [];
for (let i = 0; i < this.paddingY; i++) {
const line = bgFn ? applyBackgroundToLine(emptyLine, width, bgFn) : emptyLine;
@@ -326,7 +326,7 @@ export class Markdown implements Component {
}
}
const codeIndent = " ".repeat(this.codeBlockIndent);
const codeIndent = padding(this.codeBlockIndent);
lines.push(this.theme.codeBlockBorder(`\`\`\`${token.lang || ""}`));
if (this.theme.highlightCode) {
const highlightedLines = this.theme.highlightCode(token.text, token.lang);
@@ -577,7 +577,7 @@ export class Markdown implements Component {
lines.push(text);
} else if (token.type === "code") {
// Code block in list item
const codeIndent = " ".repeat(this.codeBlockIndent);
const codeIndent = padding(this.codeBlockIndent);
lines.push(this.theme.codeBlockBorder(`\`\`\`${token.lang || ""}`));
if (this.theme.highlightCode) {
const highlightedLines = this.theme.highlightCode(token.text, token.lang);
@@ -763,7 +763,7 @@ export class Markdown implements Component {
for (let lineIdx = 0; lineIdx < headerLineCount; lineIdx++) {
const rowParts = headerCellLines.map((cellLines, colIdx) => {
const text = cellLines[lineIdx] || "";
const padded = text + " ".repeat(Math.max(0, columnWidths[colIdx] - visibleWidth(text)));
const padded = text + padding(Math.max(0, columnWidths[colIdx] - visibleWidth(text)));
return this.theme.bold(padded);
});
lines.push(`${v} ${rowParts.join(` ${v} `)} ${v}`);
@@ -786,7 +786,7 @@ export class Markdown implements Component {
for (let lineIdx = 0; lineIdx < rowLineCount; lineIdx++) {
const rowParts = rowCellLines.map((cellLines, colIdx) => {
const text = cellLines[lineIdx] || "";
return text + " ".repeat(Math.max(0, columnWidths[colIdx] - visibleWidth(text)));
return text + padding(Math.max(0, columnWidths[colIdx] - visibleWidth(text)));
});
lines.push(`${v} ${rowParts.join(` ${v} `)} ${v}`);
}
+4 -4
View File
@@ -1,7 +1,7 @@
import { matchesKey } from "../keys";
import type { SymbolTheme } from "../symbols";
import type { Component } from "../tui";
import { truncateToWidth, visibleWidth } from "../utils";
import { padding, truncateToWidth, visibleWidth } from "../utils";
export interface SelectItem {
value: string;
@@ -84,7 +84,7 @@ export class SelectList implements Component {
// Calculate how much space we have for value + description
const maxValueWidth = Math.min(30, width - prefixWidth - 4);
const truncatedValue = truncateToWidth(displayValue, maxValueWidth, "");
const spacing = " ".repeat(Math.max(1, 32 - truncatedValue.length));
const spacing = padding(Math.max(1, 32 - truncatedValue.length));
// Calculate remaining space for description using visible widths
const descriptionStart = prefixWidth + truncatedValue.length + spacing.length;
@@ -106,13 +106,13 @@ export class SelectList implements Component {
}
} else {
const displayValue = item.label || item.value;
const prefix = " ".repeat(visibleWidth(`${this.theme.symbols.cursor} `));
const prefix = padding(visibleWidth(this.theme.symbols.cursor) + 1);
if (item.description && width > 40) {
// Calculate how much space we have for value + description
const maxValueWidth = Math.min(30, width - prefix.length - 4);
const truncatedValue = truncateToWidth(displayValue, maxValueWidth, "");
const spacing = " ".repeat(Math.max(1, 32 - truncatedValue.length));
const spacing = padding(Math.max(1, 32 - truncatedValue.length));
// Calculate remaining space for description
const descriptionStart = prefix.length + truncatedValue.length + spacing.length;
+2 -2
View File
@@ -1,6 +1,6 @@
import { matchesKey } from "../keys";
import type { Component } from "../tui";
import { truncateToWidth, visibleWidth, wrapTextWithAnsi } from "../utils";
import { padding, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "../utils";
export interface SettingItem {
/** Unique identifier for this setting */
@@ -100,7 +100,7 @@ export class SettingsList implements Component {
const prefixWidth = visibleWidth(prefix);
// Pad label to align values
const labelPadded = item.label + " ".repeat(Math.max(0, maxLabelWidth - visibleWidth(item.label)));
const labelPadded = item.label + padding(Math.max(0, maxLabelWidth - visibleWidth(item.label)));
const labelText = this.theme.label(labelPadded, isSelected);
// Calculate space for value
+4 -4
View File
@@ -1,5 +1,5 @@
import type { Component } from "../tui";
import { applyBackgroundToLine, wrapTextWithAnsi } from "../utils";
import { applyBackgroundToLine, padding, wrapTextWithAnsi } from "../utils";
/**
* Text component - displays multi-line text with word wrapping
@@ -71,8 +71,8 @@ export class Text implements Component {
const wrappedLines = wrapTextWithAnsi(normalizedText, contentWidth);
// Add margins and background to each line
const leftMargin = " ".repeat(this.paddingX);
const rightMargin = " ".repeat(this.paddingX);
const leftMargin = padding(this.paddingX);
const rightMargin = padding(this.paddingX);
const contentLines: string[] = [];
for (const line of wrappedLines) {
@@ -89,7 +89,7 @@ export class Text implements Component {
}
// Add top/bottom padding (empty lines)
const emptyLine = " ".repeat(width);
const emptyLine = padding(width);
const emptyLines: string[] = [];
for (let i = 0; i < this.paddingY; i++) {
const line = this.customBgFn ? applyBackgroundToLine(emptyLine, width, this.customBgFn) : emptyLine;
@@ -1,5 +1,5 @@
import type { Component } from "../tui";
import { truncateToWidth } from "../utils";
import { padding, truncateToWidth } from "../utils";
/**
* Text component that truncates to fit viewport width
@@ -23,7 +23,7 @@ export class TruncatedText implements Component {
const result: string[] = [];
// Empty line padded to width
const emptyLine = " ".repeat(width);
const emptyLine = padding(width);
// Add vertical padding above
for (let i = 0; i < this.paddingY; i++) {
@@ -44,8 +44,8 @@ export class TruncatedText implements Component {
const displayText = truncateToWidth(singleLineText, availableWidth);
// Add horizontal padding
const leftPadding = " ".repeat(this.paddingX);
const rightPadding = " ".repeat(this.paddingX);
const leftPadding = padding(this.paddingX);
const rightPadding = padding(this.paddingX);
const lineWithPadding = leftPadding + displayText + rightPadding;
// Don't pad to full width - avoids trailing spaces when copying
+1 -1
View File
@@ -85,4 +85,4 @@ export {
} from "./terminal-image";
export { type Component, Container, type OverlayHandle, type SizeValue, TUI } from "./tui";
// Utilities
export { truncateToWidth, visibleWidth, wrapTextWithAnsi } from "./utils";
export { padding, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "./utils";
+2 -9
View File
@@ -7,7 +7,7 @@ import * as path from "node:path";
import { isKeyRelease, matchesKey } from "./keys";
import type { Terminal } from "./terminal";
import { setCellDimensions, TERMINAL_INFO } from "./terminal-image";
import { extractSegments, sliceByColumn, sliceWithWidth, visibleWidth } from "./utils";
import { extractSegments, padding, sliceByColumn, sliceWithWidth, visibleWidth } from "./utils";
/**
* Component interface - all components must implement this
@@ -861,14 +861,7 @@ export class TUI extends Container {
// Compose result
const r = TUI.SEGMENT_RESET;
const result =
base.before +
" ".repeat(beforePad) +
r +
overlay.text +
" ".repeat(overlayPad) +
r +
base.after +
" ".repeat(afterPad);
base.before + padding(beforePad) + r + overlay.text + padding(overlayPad) + r + base.after + padding(afterPad);
// CRITICAL: Always verify and truncate to terminal width.
// This is the final safeguard against width overflow which would crash the TUI.
+13 -2
View File
@@ -5,6 +5,18 @@ import {
visibleWidth as nativeVisibleWidth,
} from "@oh-my-pi/pi-natives";
// Pre-allocated space buffer for padding
const SPACE_BUFFER = " ".repeat(512);
/**
* Returns a string of n spaces. Uses a pre-allocated buffer for efficiency.
*/
export function padding(n: number): string {
if (n <= 0) return "";
if (n <= 512) return SPACE_BUFFER.slice(0, n);
return " ".repeat(n);
}
// Grapheme segmenter (shared instance)
const segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
@@ -155,10 +167,9 @@ export function applyBackgroundToLine(line: string, width: number, bgFn: (text:
// Calculate padding needed
const visibleLen = visibleWidth(line);
const paddingNeeded = Math.max(0, width - visibleLen);
const padding = " ".repeat(paddingNeeded);
// Apply background to content + padding
const withPadding = line + padding;
const withPadding = line + padding(paddingNeeded);
return bgFn(withPadding);
}