Merge pull request #1477 from frizynn/refactor/tree-render-shared-utils
refactor: extract shared tree rendering utilities
This commit is contained in:
commit
66bca9c8a2
3 changed files with 111 additions and 38 deletions
|
|
@ -20,6 +20,12 @@ import { theme } from "../theme/theme.js";
|
|||
import { DynamicBorder } from "./dynamic-border.js";
|
||||
import { appKey, appKeyHint, keyHint } from "./keybinding-hints.js";
|
||||
import { filterAndSortSessions, hasSessionName, type NameFilter, type SortMode } from "./session-selector-search.js";
|
||||
import {
|
||||
applyRowHighlight,
|
||||
buildTreePrefix,
|
||||
computeScrollWindow,
|
||||
renderCursor,
|
||||
} from "./tree-render-utils.js";
|
||||
|
||||
type SessionScope = "current" | "all";
|
||||
|
||||
|
|
@ -420,11 +426,11 @@ class SessionList implements Component, Focusable {
|
|||
}
|
||||
|
||||
// Calculate visible range with scrolling
|
||||
const startIndex = Math.max(
|
||||
0,
|
||||
Math.min(this.selectedIndex - Math.floor(this.maxVisible / 2), this.filteredSessions.length - this.maxVisible),
|
||||
const { startIndex, endIndex } = computeScrollWindow(
|
||||
this.selectedIndex,
|
||||
this.filteredSessions.length,
|
||||
this.maxVisible,
|
||||
);
|
||||
const endIndex = Math.min(startIndex + this.maxVisible, this.filteredSessions.length);
|
||||
|
||||
// Render visible sessions (one line each with tree structure)
|
||||
for (let i = startIndex; i < endIndex; i++) {
|
||||
|
|
@ -435,7 +441,7 @@ class SessionList implements Component, Focusable {
|
|||
const isCurrent = this.currentSessionFilePath === session.path;
|
||||
|
||||
// Build tree prefix
|
||||
const prefix = this.buildTreePrefix(node);
|
||||
const prefix = this.buildNodeTreePrefix(node);
|
||||
|
||||
// Session display text (name or first message)
|
||||
const hasName = !!session.name;
|
||||
|
|
@ -454,7 +460,7 @@ class SessionList implements Component, Focusable {
|
|||
}
|
||||
|
||||
// Cursor
|
||||
const cursor = isSelected ? theme.fg("accent", "› ") : " ";
|
||||
const cursor = renderCursor(isSelected);
|
||||
|
||||
// Calculate available width for message
|
||||
const prefixWidth = visibleWidth(prefix);
|
||||
|
|
@ -483,11 +489,8 @@ class SessionList implements Component, Focusable {
|
|||
const spacing = Math.max(1, width - leftWidth - visibleWidth(rightPart));
|
||||
const styledRight = theme.fg(isConfirmingDelete ? "error" : "dim", rightPart);
|
||||
|
||||
let line = leftPart + " ".repeat(spacing) + styledRight;
|
||||
if (isSelected) {
|
||||
line = theme.bg("selectedBg", line);
|
||||
}
|
||||
lines.push(truncateToWidth(line, width));
|
||||
const line = leftPart + " ".repeat(spacing) + styledRight;
|
||||
lines.push(applyRowHighlight(line, isSelected, width));
|
||||
}
|
||||
|
||||
// Add scroll indicator if needed
|
||||
|
|
@ -500,14 +503,8 @@ class SessionList implements Component, Focusable {
|
|||
return lines;
|
||||
}
|
||||
|
||||
private buildTreePrefix(node: FlatSessionNode): string {
|
||||
if (node.depth === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const parts = node.ancestorContinues.map((continues) => (continues ? "│ " : " "));
|
||||
const branch = node.isLast ? "└─ " : "├─ ";
|
||||
return parts.join("") + branch;
|
||||
private buildNodeTreePrefix(node: FlatSessionNode): string {
|
||||
return buildTreePrefix(node.ancestorContinues, node.isLast, node.depth);
|
||||
}
|
||||
|
||||
handleInput(keyData: string): void {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,81 @@
|
|||
import { truncateToWidth } from "@gsd/pi-tui";
|
||||
import { theme } from "../theme/theme.js";
|
||||
|
||||
// ── Tree connector characters ────────────────────────────────────────
|
||||
export const TREE_BRANCH = "\u251C\u2500 "; // "├─ "
|
||||
export const TREE_LAST = "\u2514\u2500 "; // "└─ "
|
||||
export const TREE_PIPE = "\u2502 "; // "│ "
|
||||
export const TREE_SPACE = " "; // 3 spaces
|
||||
|
||||
/**
|
||||
* Build a tree prefix string from ancestor-continuation flags and branch position.
|
||||
*
|
||||
* Each ancestor level contributes either a pipe ("│ ") or blank spacing (" ")
|
||||
* depending on whether that ancestor has more siblings after it. The final segment
|
||||
* is the branch connector: "├─ " (more siblings) or "└─ " (last sibling).
|
||||
*
|
||||
* Used by session-selector for its simpler flat tree display.
|
||||
* tree-selector uses its own gutter-based char-by-char builder for richer rendering.
|
||||
*/
|
||||
export function buildTreePrefix(ancestorContinues: boolean[], isLast: boolean, depth: number): string {
|
||||
if (depth === 0) return "";
|
||||
const parts = ancestorContinues.map((continues) => (continues ? TREE_PIPE : TREE_SPACE));
|
||||
const branch = isLast ? TREE_LAST : TREE_BRANCH;
|
||||
return parts.join("") + branch;
|
||||
}
|
||||
|
||||
// ── Scroll window ────────────────────────────────────────────────────
|
||||
|
||||
export interface ScrollWindow {
|
||||
/** First visible index (inclusive) */
|
||||
startIndex: number;
|
||||
/** Last visible index (exclusive) */
|
||||
endIndex: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute a centered scroll window around `selectedIndex` within a list of `totalItems`.
|
||||
*
|
||||
* The window tries to center the selected item. When near the beginning or end of the
|
||||
* list the window clamps so it doesn't exceed bounds.
|
||||
*/
|
||||
export function computeScrollWindow(selectedIndex: number, totalItems: number, maxVisible: number): ScrollWindow {
|
||||
const startIndex = Math.max(0, Math.min(selectedIndex - Math.floor(maxVisible / 2), totalItems - maxVisible));
|
||||
const endIndex = Math.min(startIndex + maxVisible, totalItems);
|
||||
return { startIndex, endIndex };
|
||||
}
|
||||
|
||||
// ── Cursor & selection helpers ───────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Return the cursor indicator for a list row.
|
||||
*
|
||||
* Selected: "› " (accent-colored)
|
||||
* Unselected: " " (two spaces, matching width)
|
||||
*/
|
||||
export function renderCursor(isSelected: boolean): string {
|
||||
return isSelected ? theme.fg("accent", "\u203A ") : " ";
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply selected-row background highlight and truncate to `width`.
|
||||
*/
|
||||
export function applyRowHighlight(line: string, isSelected: boolean, width: number): string {
|
||||
const truncated = truncateToWidth(line, width);
|
||||
return isSelected ? theme.bg("selectedBg", truncated) : truncated;
|
||||
}
|
||||
|
||||
// ── Scroll position indicator ────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Render a muted "(current/total)" position indicator, optionally with a suffix label.
|
||||
*/
|
||||
export function renderScrollPosition(
|
||||
selectedIndex: number,
|
||||
totalItems: number,
|
||||
width: number,
|
||||
suffixLabel?: string,
|
||||
): string {
|
||||
const suffix = suffixLabel ?? "";
|
||||
return truncateToWidth(theme.fg("muted", ` (${selectedIndex + 1}/${totalItems})${suffix}`), width);
|
||||
}
|
||||
|
|
@ -14,6 +14,12 @@ import type { SessionTreeNode } from "../../../core/session-manager.js";
|
|||
import { theme } from "../theme/theme.js";
|
||||
import { DynamicBorder } from "./dynamic-border.js";
|
||||
import { keyHint } from "./keybinding-hints.js";
|
||||
import {
|
||||
applyRowHighlight,
|
||||
computeScrollWindow,
|
||||
renderCursor,
|
||||
renderScrollPosition,
|
||||
} from "./tree-render-utils.js";
|
||||
|
||||
/** Gutter info: position (displayIndent where connector was) and whether to show │ */
|
||||
interface GutterInfo {
|
||||
|
|
@ -595,14 +601,11 @@ class TreeList implements Component {
|
|||
return lines;
|
||||
}
|
||||
|
||||
const startIndex = Math.max(
|
||||
0,
|
||||
Math.min(
|
||||
this.selectedIndex - Math.floor(this.maxVisibleLines / 2),
|
||||
this.filteredNodes.length - this.maxVisibleLines,
|
||||
),
|
||||
const { startIndex, endIndex } = computeScrollWindow(
|
||||
this.selectedIndex,
|
||||
this.filteredNodes.length,
|
||||
this.maxVisibleLines,
|
||||
);
|
||||
const endIndex = Math.min(startIndex + this.maxVisibleLines, this.filteredNodes.length);
|
||||
|
||||
for (let i = startIndex; i < endIndex; i++) {
|
||||
const flatNode = this.filteredNodes[i];
|
||||
|
|
@ -610,7 +613,7 @@ class TreeList implements Component {
|
|||
const isSelected = i === this.selectedIndex;
|
||||
|
||||
// Build line: cursor + prefix + path marker + label + content
|
||||
const cursor = isSelected ? theme.fg("accent", "› ") : " ";
|
||||
const cursor = renderCursor(isSelected);
|
||||
|
||||
// If multiple roots, shift display (roots at 0, not 1)
|
||||
const displayIndent = this.multipleRoots ? Math.max(0, flatNode.indent - 1) : flatNode.indent;
|
||||
|
|
@ -664,19 +667,11 @@ class TreeList implements Component {
|
|||
const label = flatNode.node.label ? theme.fg("warning", `[${flatNode.node.label}] `) : "";
|
||||
const content = this.getEntryDisplayText(flatNode.node, isSelected);
|
||||
|
||||
let line = cursor + theme.fg("dim", prefix) + foldMarker + pathMarker + label + content;
|
||||
if (isSelected) {
|
||||
line = theme.bg("selectedBg", line);
|
||||
}
|
||||
lines.push(truncateToWidth(line, width));
|
||||
const line = cursor + theme.fg("dim", prefix) + foldMarker + pathMarker + label + content;
|
||||
lines.push(applyRowHighlight(line, isSelected, width));
|
||||
}
|
||||
|
||||
lines.push(
|
||||
truncateToWidth(
|
||||
theme.fg("muted", ` (${this.selectedIndex + 1}/${this.filteredNodes.length})${this.getFilterLabel()}`),
|
||||
width,
|
||||
),
|
||||
);
|
||||
lines.push(renderScrollPosition(this.selectedIndex, this.filteredNodes.length, width, this.getFilterLabel()));
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue