Files
oh-my-pi/packages/coding-agent/src/tui/tree-list.ts
T
can1357 9b78a6fa85 refactor(natives): migrated native bindings to NAPI-RS with auto-generated types
- Migrated native bindings from TypeScript wrappers to NAPI-RS generated modules with auto-generated type definitions and runtime enums.
- Replaced chunk tree API with stateful ChunkState class supporting render, edit, and resolve operations with improved error handling.
- Converted callback signatures to error-first pattern (error, result) for shell, PTY, glob, and grep operations.
- Introduced type-safe enums for MacOSAppearance, GrepOutputMode, KeyEventType, ImageFormat, and AstMatchStrictness replacing string literals.
- Refactored chunk tree implementation with dedicated modules for edit, indent, resolve, and state management with comprehensive validation.
- Moved clipboard utilities from native bindings to coding-agent package with improved OSC 52 and Termux compatibility.
2026-04-07 02:00:59 +02:00

85 lines
3.1 KiB
TypeScript

/**
* Hierarchical tree list rendering helper.
*/
import { replaceTabs } from "@oh-my-pi/pi-tui";
import type { Theme } from "../modes/theme/theme";
import { formatMoreItems } from "../tools/render-utils";
import type { TreeContext } from "./types";
import { getTreeBranch, getTreeContinuePrefix } from "./utils";
export interface TreeListOptions<T> {
items: T[];
expanded?: boolean;
maxCollapsed?: number;
/** Strict total-line budget for collapsed mode. When set (and not expanded),
* rendered item lines plus the trailing summary line must fit within this budget.
*/
maxCollapsedLines?: number;
itemType?: string;
/** Called once per item with `isLast: false` during budget calculation;
* line count MUST NOT vary based on `isLast`. */
renderItem: (item: T, context: TreeContext) => string | string[];
}
export function renderTreeList<T>(options: TreeListOptions<T>, theme: Theme): string[] {
const { items, expanded = false, maxCollapsed = 8, maxCollapsedLines, itemType = "item", renderItem } = options;
const maxItems = expanded ? items.length : Math.min(items.length, maxCollapsed);
const linesBudget = !expanded && maxCollapsedLines !== undefined ? maxCollapsedLines : Infinity;
// Pre-render each candidate item once.
// isLast cannot be known at this point (fittingCount is not yet determined);
// renderItem implementations MUST NOT vary line count based on isLast.
const preRendered: string[][] = [];
for (let i = 0; i < maxItems; i++) {
const rendered = renderItem(items[i], {
index: i,
isLast: false,
depth: 0,
theme,
prefix: "",
continuePrefix: "",
});
preRendered.push(Array.isArray(rendered) ? rendered : rendered ? [rendered] : []);
}
// Determine how many items fit within the line budget.
let fittingCount = maxItems;
let fittedLineCount = 0;
if (linesBudget !== Infinity) {
fittingCount = 0;
for (let i = 0; i < maxItems; i++) {
const count = preRendered[i]!.length;
const remainingAfter = items.length - (i + 1);
const reservedSummaryLines = remainingAfter > 0 ? 1 : 0;
if (fittedLineCount + count + reservedSummaryLines > linesBudget) break;
fittedLineCount += count;
fittingCount = i + 1;
}
}
const remaining = items.length - fittingCount;
const hasSummary = !expanded && remaining > 0 && (linesBudget === Infinity || fittedLineCount < linesBudget);
// Emit pre-rendered content with correct isLast-based branch prefixes.
const lines: string[] = [];
for (let i = 0; i < fittingCount; i++) {
const isLast = !hasSummary && i === fittingCount - 1;
const branch = getTreeBranch(isLast, theme);
const prefix = `${theme.fg("dim", branch)} `;
const continuePrefix = `${theme.fg("dim", getTreeContinuePrefix(isLast, theme))}`;
const itemLines = preRendered[i]!;
if (itemLines.length === 0) continue;
lines.push(`${prefix}${replaceTabs(itemLines[0]!)}`);
for (let j = 1; j < itemLines.length; j++) {
lines.push(`${continuePrefix}${replaceTabs(itemLines[j]!)}`);
}
}
if (hasSummary) {
lines.push(`${theme.fg("dim", theme.tree.last)} ${theme.fg("muted", formatMoreItems(remaining, itemType))}`);
}
return lines;
}