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:
@@ -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"] {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user