Something went wrong. Try again.
Shared settings UI and config loader for pi extensions
Something went wrong. Try again.
6.2 kB · 222 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223import type { Component, SettingsListTheme } from "@earendil-works/pi-tui";import { Input, Key, matchesKey, truncateToWidth, visibleWidth,} from "@earendil-works/pi-tui";import { renderSettingsPanel } from "./render-settings-panel";
/** * A submenu component for editing string arrays inside a SettingsList. * * Modes: * - list: navigate items, delete with 'd', add with 'a', edit with 'e'/Enter * - add: text input for new item, confirm with Enter, cancel with Escape * - edit: text input pre-filled with current value, Enter saves, Escape cancels */
export interface ArrayEditorOptions { label: string; items: string[]; theme: SettingsListTheme; onSave: (items: string[]) => void; onDone: () => void; /** Max visible items before scrolling */ maxVisible?: number; /** * Save hook invoked on Ctrl+S. Only useful when the editor is used * standalone; registerSettingsCommand intercepts Ctrl+S at the root. */ requestSave?: () => void;}
export class ArrayEditor implements Component { private items: string[]; private label: string; private theme: SettingsListTheme; private onSave: (items: string[]) => void; private onDone: () => void; private requestSave: () => void; private selectedIndex = 0; private maxVisible: number; private mode: "list" | "add" | "edit" = "list"; private input: Input; private editIndex = -1;
constructor(options: ArrayEditorOptions) { this.items = [...options.items]; this.label = options.label; this.theme = options.theme; this.onSave = options.onSave; this.onDone = options.onDone; this.requestSave = options.requestSave ?? (() => {}); this.maxVisible = options.maxVisible ?? 10; this.input = new Input(); this.input.onSubmit = (value: string) => { if (this.mode === "edit") { this.submitEdit(value); } else { this.submitAdd(value); } }; this.input.onEscape = () => { this.mode = "list"; this.editIndex = -1; }; }
private submitAdd(value: string) { const trimmed = value.trim(); if (!trimmed) { this.mode = "list"; return; } this.items.push(trimmed); this.selectedIndex = this.items.length - 1; this.save(); this.mode = "list"; this.input.setValue(""); }
private submitEdit(value: string) { const trimmed = value.trim(); if (!trimmed) { // Empty value = cancel edit this.mode = "list"; this.editIndex = -1; return; } this.items[this.editIndex] = trimmed; this.save(); this.mode = "list"; this.editIndex = -1; this.input.setValue(""); }
private deleteSelected() { if (this.items.length === 0) return; this.items.splice(this.selectedIndex, 1); if (this.selectedIndex >= this.items.length) { this.selectedIndex = Math.max(0, this.items.length - 1); } this.save(); }
private startEdit() { if (this.items.length === 0) return; this.editIndex = this.selectedIndex; this.mode = "edit"; this.input.setValue(this.items[this.selectedIndex] as string); }
private save() { this.onSave([...this.items]); }
invalidate() {}
render(width: number): string[] { return renderSettingsPanel(width, this.label, this.theme, (innerWidth) => { if (this.mode === "add" || this.mode === "edit") { return this.renderInputMode(innerWidth); }
return this.renderListMode(innerWidth); }); }
private renderListMode(width: number): string[] { const lines: string[] = [];
if (this.items.length === 0) { lines.push(this.theme.hint(" (empty)")); } else { const startIndex = Math.max( 0, Math.min( this.selectedIndex - Math.floor(this.maxVisible / 2), this.items.length - this.maxVisible, ), ); const endIndex = Math.min( startIndex + this.maxVisible, this.items.length, );
for (let i = startIndex; i < endIndex; i++) { const item = this.items[i]; if (!item) continue; const isSelected = i === this.selectedIndex; const prefix = isSelected ? this.theme.cursor : " "; const prefixWidth = visibleWidth(prefix); const maxItemWidth = width - prefixWidth - 2; const text = this.theme.value( truncateToWidth(item, maxItemWidth, ""), isSelected, ); lines.push(prefix + text); }
if (startIndex > 0 || endIndex < this.items.length) { lines.push( this.theme.hint(` (${this.selectedIndex + 1}/${this.items.length})`), ); } }
lines.push(""); lines.push( this.theme.hint(" a: add · e/Enter: edit · d: delete · Esc: back"), );
return lines; }
private renderInputMode(width: number): string[] { const lines: string[] = []; const label = this.mode === "edit" ? " Edit item:" : " New item:"; lines.push(this.theme.hint(label)); lines.push(` ${this.input.render(width - 4).join("")}`); lines.push(""); lines.push(this.theme.hint(" Enter: confirm · Esc: cancel")); return lines; }
handleInput(data: string) { if (matchesKey(data, Key.ctrl("s"))) { this.requestSave(); return; }
if (this.mode === "add" || this.mode === "edit") { this.input.handleInput(data); return; }
// List mode if (matchesKey(data, Key.up) || data === "k") { if (this.items.length === 0) return; this.selectedIndex = this.selectedIndex === 0 ? this.items.length - 1 : this.selectedIndex - 1; } else if (matchesKey(data, Key.down) || data === "j") { if (this.items.length === 0) return; this.selectedIndex = this.selectedIndex === this.items.length - 1 ? 0 : this.selectedIndex + 1; } else if (data === "a" || data === "A") { this.mode = "add"; this.input.setValue(""); } else if (data === "e" || data === "E" || matchesKey(data, Key.enter)) { this.startEdit(); } else if (data === "d" || data === "D") { this.deleteSelected(); } else if (matchesKey(data, Key.escape)) { this.onDone(); } }}