mirror of
https://github.com/earendil-works/pi.git
synced 2026-06-18 15:54:04 +08:00
@@ -6,6 +6,7 @@ import {
|
||||
Input,
|
||||
type Keybinding,
|
||||
Spacer,
|
||||
sliceByColumn,
|
||||
Text,
|
||||
truncateToWidth,
|
||||
visibleWidth,
|
||||
@@ -37,6 +38,59 @@ interface FlatNode {
|
||||
isVirtualRootChild: boolean;
|
||||
}
|
||||
|
||||
interface HorizontalViewportRow {
|
||||
gutter: string;
|
||||
body: string;
|
||||
anchorCol: number;
|
||||
bodyWidth: number;
|
||||
isSelected: boolean;
|
||||
}
|
||||
|
||||
const TREE_GUTTER_WIDTH = 2;
|
||||
const MIN_VISIBLE_ANCHOR_CONTENT_WIDTH = 4;
|
||||
const MAX_VISIBLE_ANCHOR_CONTENT_WIDTH = 20;
|
||||
const MIN_ANCHOR_CONTEXT_WIDTH = 2;
|
||||
const MAX_ANCHOR_CONTEXT_WIDTH = 12;
|
||||
|
||||
/**
|
||||
* Render tree rows into a horizontally clipped viewport.
|
||||
*
|
||||
* The tree gutter is always kept visible. The row bodies are shifted left only
|
||||
* when the selected row's anchor (the start of its entry text after tree
|
||||
* indentation/markers) would otherwise be too far right to see useful content.
|
||||
*/
|
||||
function renderHorizontalViewport(rows: HorizontalViewportRow[], width: number): string[] {
|
||||
const viewportWidth = Math.max(0, width - TREE_GUTTER_WIDTH);
|
||||
const maxBodyWidth = rows.reduce((max, row) => Math.max(max, row.bodyWidth), 0);
|
||||
const maxHorizontalScroll = Math.max(0, maxBodyWidth - viewportWidth);
|
||||
const selectedRow = rows.find((row) => row.isSelected);
|
||||
|
||||
// Only pan horizontally when needed to keep enough selected-row content visible after its anchor.
|
||||
let horizontalScroll = 0;
|
||||
if (selectedRow && maxHorizontalScroll > 0) {
|
||||
const minVisibleAnchorContentWidth = Math.min(
|
||||
MAX_VISIBLE_ANCHOR_CONTENT_WIDTH,
|
||||
Math.max(MIN_VISIBLE_ANCHOR_CONTENT_WIDTH, Math.floor(viewportWidth / 3)),
|
||||
);
|
||||
if (selectedRow.anchorCol > viewportWidth - minVisibleAnchorContentWidth) {
|
||||
const anchorContextWidth = Math.min(
|
||||
MAX_ANCHOR_CONTEXT_WIDTH,
|
||||
Math.max(MIN_ANCHOR_CONTEXT_WIDTH, Math.floor(viewportWidth / 4)),
|
||||
);
|
||||
horizontalScroll = Math.min(maxHorizontalScroll, selectedRow.anchorCol - anchorContextWidth);
|
||||
}
|
||||
}
|
||||
|
||||
// Clip only the body; the fixed-width gutter remains visible as navigation context.
|
||||
return rows.map((row) => {
|
||||
const line =
|
||||
horizontalScroll > 0
|
||||
? `${row.gutter}${sliceByColumn(row.body, horizontalScroll, viewportWidth, true)}\x1b[0m`
|
||||
: row.gutter + row.body;
|
||||
return truncateToWidth(line, width, "");
|
||||
});
|
||||
}
|
||||
|
||||
/** Filter mode for tree display */
|
||||
export type FilterMode = "default" | "no-tools" | "user-only" | "labeled-only" | "all";
|
||||
|
||||
@@ -619,6 +673,7 @@ class TreeList implements Component {
|
||||
);
|
||||
const endIndex = Math.min(startIndex + this.maxVisibleLines, this.filteredNodes.length);
|
||||
|
||||
const renderedRows: HorizontalViewportRow[] = [];
|
||||
for (let i = startIndex; i < endIndex; i++) {
|
||||
const flatNode = this.filteredNodes[i];
|
||||
const entry = flatNode.node.entry;
|
||||
@@ -682,14 +737,18 @@ class TreeList implements Component {
|
||||
? theme.fg("muted", `${this.formatLabelTimestamp(flatNode.node.labelTimestamp)} `)
|
||||
: "";
|
||||
const content = this.getEntryDisplayText(flatNode.node, isSelected);
|
||||
|
||||
let line = cursor + theme.fg("dim", prefix) + foldMarker + pathMarker + label + labelTimestamp + content;
|
||||
const prefixPart = theme.fg("dim", prefix) + foldMarker + pathMarker;
|
||||
const anchorCol = visibleWidth(prefixPart);
|
||||
let gutter = cursor;
|
||||
let body = prefixPart + label + labelTimestamp + content;
|
||||
if (isSelected) {
|
||||
line = theme.bg("selectedBg", line);
|
||||
gutter = theme.bg("selectedBg", gutter);
|
||||
body = theme.bg("selectedBg", body);
|
||||
}
|
||||
lines.push(truncateToWidth(line, width));
|
||||
renderedRows.push({ gutter, body, anchorCol, bodyWidth: visibleWidth(body), isSelected });
|
||||
}
|
||||
|
||||
lines.push(...renderHorizontalViewport(renderedRows, width));
|
||||
lines.push(
|
||||
truncateToWidth(
|
||||
theme.fg("muted", ` (${this.selectedIndex + 1}/${this.filteredNodes.length})${this.getStatusLabels()}`),
|
||||
|
||||
@@ -106,4 +106,4 @@ export {
|
||||
TUI,
|
||||
} from "./tui.ts";
|
||||
// Utilities
|
||||
export { truncateToWidth, visibleWidth, wrapTextWithAnsi } from "./utils.ts";
|
||||
export { sliceByColumn, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "./utils.ts";
|
||||
|
||||
Reference in New Issue
Block a user