/** * Generic selector component for extensions. * Displays a list of string options with keyboard navigation. */ import { Container, getEditorKeybindings, Spacer, Text, type TUI } from "@mariozechner/pi-tui"; import { theme } from "../theme/theme.js"; import { CountdownTimer } from "./countdown-timer.js"; import { DynamicBorder } from "./dynamic-border.js"; export interface ExtensionSelectorOptions { tui?: TUI; timeout?: number; } export class ExtensionSelectorComponent extends Container { private options: string[]; private selectedIndex = 0; private listContainer: Container; private onSelectCallback: (option: string) => void; private onCancelCallback: () => void; private titleText: Text; private baseTitle: string; private countdown: CountdownTimer | undefined; constructor( title: string, options: string[], onSelect: (option: string) => void, onCancel: () => void, opts?: ExtensionSelectorOptions, ) { super(); this.options = options; this.onSelectCallback = onSelect; this.onCancelCallback = onCancel; this.baseTitle = title; this.addChild(new DynamicBorder()); this.addChild(new Spacer(1)); this.titleText = new Text(theme.fg("accent", title), 1, 0); this.addChild(this.titleText); this.addChild(new Spacer(1)); if (opts?.timeout && opts.timeout > 0 && opts.tui) { this.countdown = new CountdownTimer( opts.timeout, opts.tui, (s) => this.titleText.setText(theme.fg("accent", `${this.baseTitle} (${s}s)`)), () => this.onCancelCallback(), ); } this.listContainer = new Container(); this.addChild(this.listContainer); this.addChild(new Spacer(1)); this.addChild(new Text(theme.fg("dim", "↑↓ navigate enter select esc cancel"), 1, 0)); this.addChild(new Spacer(1)); this.addChild(new DynamicBorder()); this.updateList(); } private updateList(): void { this.listContainer.clear(); for (let i = 0; i < this.options.length; i++) { const isSelected = i === this.selectedIndex; const text = isSelected ? theme.fg("accent", "→ ") + theme.fg("accent", this.options[i]) : ` ${theme.fg("text", this.options[i])}`; this.listContainer.addChild(new Text(text, 1, 0)); } } handleInput(keyData: string): void { const kb = getEditorKeybindings(); if (kb.matches(keyData, "selectUp") || keyData === "k") { this.selectedIndex = Math.max(0, this.selectedIndex - 1); this.updateList(); } else if (kb.matches(keyData, "selectDown") || keyData === "j") { this.selectedIndex = Math.min(this.options.length - 1, this.selectedIndex + 1); this.updateList(); } else if (kb.matches(keyData, "selectConfirm") || keyData === "\n") { const selected = this.options[this.selectedIndex]; if (selected) this.onSelectCallback(selected); } else if (kb.matches(keyData, "selectCancel")) { this.onCancelCallback(); } } dispose(): void { this.countdown?.dispose(); } }