@juanibiapina/pi-extension-settings 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Read/write extension settings to JSON files.
3
+ */
4
+
5
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
6
+ import { dirname, join } from "node:path";
7
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
8
+
9
+ const SETTINGS_FILE_NAME = "settings-extensions.json";
10
+
11
+ type SettingsFile = Record<string, Record<string, string>>;
12
+
13
+ /**
14
+ * Get the global settings file path.
15
+ */
16
+ function getGlobalSettingsPath(): string {
17
+ return join(getAgentDir(), SETTINGS_FILE_NAME);
18
+ }
19
+
20
+ /**
21
+ * Load the settings file. Returns empty object if file doesn't exist or is invalid.
22
+ */
23
+ function loadSettingsFile(path: string): SettingsFile {
24
+ if (!existsSync(path)) {
25
+ return {};
26
+ }
27
+ try {
28
+ const content = readFileSync(path, "utf-8");
29
+ return JSON.parse(content) as SettingsFile;
30
+ } catch {
31
+ return {};
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Save settings to the global file.
37
+ */
38
+ function saveSettingsFile(path: string, settings: SettingsFile): void {
39
+ const dir = dirname(path);
40
+ if (!existsSync(dir)) {
41
+ mkdirSync(dir, { recursive: true });
42
+ }
43
+ writeFileSync(path, JSON.stringify(settings, null, "\t"));
44
+ }
45
+
46
+ /**
47
+ * Get a setting value for an extension.
48
+ * Returns the stored value, or the provided default, or undefined.
49
+ *
50
+ * @param extensionName - Extension name
51
+ * @param settingId - Setting ID within the extension
52
+ * @param defaultValue - Default value if setting is not found
53
+ * @returns The setting value
54
+ */
55
+ export function getSetting(extensionName: string, settingId: string, defaultValue?: string): string | undefined {
56
+ const globalPath = getGlobalSettingsPath();
57
+ const settings = loadSettingsFile(globalPath);
58
+
59
+ // Check if value exists in file
60
+ const extSettings = settings[extensionName];
61
+ if (extSettings && settingId in extSettings) {
62
+ return extSettings[settingId];
63
+ }
64
+
65
+ return defaultValue;
66
+ }
67
+
68
+ /**
69
+ * Set a setting value for an extension.
70
+ * Always writes to the global settings file.
71
+ *
72
+ * @param extensionName - Extension name
73
+ * @param settingId - Setting ID within the extension
74
+ * @param value - Value to set
75
+ */
76
+ export function setSetting(extensionName: string, settingId: string, value: string): void {
77
+ const globalPath = getGlobalSettingsPath();
78
+ const settings = loadSettingsFile(globalPath);
79
+
80
+ if (!settings[extensionName]) {
81
+ settings[extensionName] = {};
82
+ }
83
+ settings[extensionName][settingId] = value;
84
+
85
+ saveSettingsFile(globalPath, settings);
86
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Type definitions for extension settings.
3
+ */
4
+
5
+ export interface OrderedListOption {
6
+ /** Value stored in the comma-separated setting */
7
+ id: string;
8
+ /** Display label in the menu */
9
+ label: string;
10
+ }
11
+
12
+ export interface SettingDefinition {
13
+ /** Unique identifier for this setting within the extension */
14
+ id: string;
15
+ /** Display label */
16
+ label: string;
17
+ /** Optional description shown when selected */
18
+ description?: string;
19
+ /** Default value if not set in config file */
20
+ defaultValue: string;
21
+ /**
22
+ * Values to cycle through (Enter/Space cycles).
23
+ * If undefined or empty, the setting is treated as a free-form string input.
24
+ * Mutually exclusive with `options`.
25
+ */
26
+ values?: string[];
27
+ /**
28
+ * Available options for ordered multi-select.
29
+ * When present, Enter opens an ordered list submenu where items can be
30
+ * toggled on/off and reordered. Value is stored as comma-separated IDs.
31
+ * Mutually exclusive with `values`.
32
+ */
33
+ options?: OrderedListOption[];
34
+ }
@@ -1,26 +0,0 @@
1
- /**
2
- * Ordered multi-select component.
3
- *
4
- * Displays a list of options that can be toggled on/off and reordered.
5
- * Selected items appear at the top with their position number.
6
- * Value is stored as a comma-separated string of selected IDs in order.
7
- */
8
- import { type Component } from "@earendil-works/pi-tui";
9
- import type { OrderedListOption } from "../settings/types.js";
10
- import type { SettingsListTheme } from "./settings-list.js";
11
- export declare class OrderedMultiSelect implements Component {
12
- private options;
13
- private selected;
14
- private cursorIndex;
15
- private theme;
16
- private done;
17
- constructor(options: OrderedListOption[], currentValue: string, theme: SettingsListTheme, done: (selectedValue?: string) => void);
18
- invalidate(): void;
19
- render(width: number): string[];
20
- handleInput(data: string): void;
21
- private buildDisplayItems;
22
- private toggleCurrent;
23
- private moveUp;
24
- private moveDown;
25
- }
26
- //# sourceMappingURL=ordered-multi-select.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"ordered-multi-select.d.ts","sourceRoot":"","sources":["../../src/components/ordered-multi-select.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,KAAK,SAAS,EAA+C,MAAM,wBAAwB,CAAC;AACrG,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAS5D,qBAAa,kBAAmB,YAAW,SAAS;IACnD,OAAO,CAAC,OAAO,CAAsB;IACrC,OAAO,CAAC,QAAQ,CAAW;IAC3B,OAAO,CAAC,WAAW,CAAK;IACxB,OAAO,CAAC,KAAK,CAAoB;IACjC,OAAO,CAAC,IAAI,CAAmC;gBAG9C,OAAO,EAAE,iBAAiB,EAAE,EAC5B,YAAY,EAAE,MAAM,EACpB,KAAK,EAAE,iBAAiB,EACxB,IAAI,EAAE,CAAC,aAAa,CAAC,EAAE,MAAM,KAAK,IAAI;IAWvC,UAAU,IAAI,IAAI;IAElB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE;IAkC/B,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IA2B/B,OAAO,CAAC,iBAAiB;IAsBzB,OAAO,CAAC,aAAa;IAcrB,OAAO,CAAC,MAAM;IAad,OAAO,CAAC,QAAQ;CAYhB"}
@@ -1,141 +0,0 @@
1
- /**
2
- * Ordered multi-select component.
3
- *
4
- * Displays a list of options that can be toggled on/off and reordered.
5
- * Selected items appear at the top with their position number.
6
- * Value is stored as a comma-separated string of selected IDs in order.
7
- */
8
- import { getKeybindings, matchesKey, truncateToWidth } from "@earendil-works/pi-tui";
9
- export class OrderedMultiSelect {
10
- options;
11
- selected;
12
- cursorIndex = 0;
13
- theme;
14
- done;
15
- constructor(options, currentValue, theme, done) {
16
- this.options = options;
17
- this.selected = currentValue
18
- .split(",")
19
- .map((s) => s.trim())
20
- .filter(Boolean);
21
- this.theme = theme;
22
- this.done = done;
23
- }
24
- invalidate() { }
25
- render(width) {
26
- const lines = [];
27
- const items = this.buildDisplayItems();
28
- for (let i = 0; i < items.length; i++) {
29
- const item = items[i];
30
- const isCursor = i === this.cursorIndex;
31
- const prefix = isCursor ? this.theme.cursor : " ";
32
- let marker;
33
- if (item.selected) {
34
- marker = this.theme.value(`✓ ${String(item.position).padStart(2)}`, isCursor);
35
- }
36
- else {
37
- marker = this.theme.label(" ", isCursor);
38
- }
39
- const label = this.theme.label(item.option.label, isCursor);
40
- const line = `${prefix}${marker} ${label}`;
41
- lines.push(truncateToWidth(line, width, "…"));
42
- }
43
- // Scroll indicator
44
- if (items.length > 0) {
45
- const scrollText = ` (${this.cursorIndex + 1}/${items.length})`;
46
- lines.push(this.theme.hint(scrollText));
47
- }
48
- // Hint line
49
- lines.push("");
50
- lines.push(this.theme.hint(" Space toggle · Shift+↑/↓ reorder · Enter confirm · Esc cancel"));
51
- return lines;
52
- }
53
- handleInput(data) {
54
- const kb = getKeybindings();
55
- const items = this.buildDisplayItems();
56
- if (items.length === 0) {
57
- if (kb.matches(data, "tui.select.cancel")) {
58
- this.done(undefined);
59
- }
60
- return;
61
- }
62
- if (kb.matches(data, "tui.select.up") || matchesKey(data, "up")) {
63
- this.cursorIndex = this.cursorIndex === 0 ? items.length - 1 : this.cursorIndex - 1;
64
- }
65
- else if (kb.matches(data, "tui.select.down") || matchesKey(data, "down")) {
66
- this.cursorIndex = this.cursorIndex === items.length - 1 ? 0 : this.cursorIndex + 1;
67
- }
68
- else if (data === " " || matchesKey(data, "space")) {
69
- this.toggleCurrent(items);
70
- }
71
- else if (matchesKey(data, "shift+up")) {
72
- this.moveUp(items);
73
- }
74
- else if (matchesKey(data, "shift+down")) {
75
- this.moveDown(items);
76
- }
77
- else if (kb.matches(data, "tui.select.confirm")) {
78
- this.done(this.selected.join(","));
79
- }
80
- else if (kb.matches(data, "tui.select.cancel")) {
81
- this.done(undefined);
82
- }
83
- }
84
- buildDisplayItems() {
85
- const items = [];
86
- // Selected items first, in order
87
- for (let i = 0; i < this.selected.length; i++) {
88
- const id = this.selected[i];
89
- const option = this.options.find((o) => o.id === id);
90
- if (option) {
91
- items.push({ option, selected: true, position: i + 1 });
92
- }
93
- }
94
- // Unselected items, in original options order
95
- for (const option of this.options) {
96
- if (!this.selected.includes(option.id)) {
97
- items.push({ option, selected: false, position: 0 });
98
- }
99
- }
100
- return items;
101
- }
102
- toggleCurrent(items) {
103
- const item = items[this.cursorIndex];
104
- if (!item)
105
- return;
106
- const id = item.option.id;
107
- if (item.selected) {
108
- // Remove from selected
109
- this.selected = this.selected.filter((s) => s !== id);
110
- }
111
- else {
112
- // Add to end of selected
113
- this.selected.push(id);
114
- }
115
- }
116
- moveUp(items) {
117
- const item = items[this.cursorIndex];
118
- if (!item?.selected)
119
- return;
120
- const idx = this.selected.indexOf(item.option.id);
121
- if (idx <= 0)
122
- return;
123
- // Swap with previous in selected array
124
- [this.selected[idx - 1], this.selected[idx]] = [this.selected[idx], this.selected[idx - 1]];
125
- // Move cursor up to follow the item
126
- this.cursorIndex--;
127
- }
128
- moveDown(items) {
129
- const item = items[this.cursorIndex];
130
- if (!item?.selected)
131
- return;
132
- const idx = this.selected.indexOf(item.option.id);
133
- if (idx < 0 || idx >= this.selected.length - 1)
134
- return;
135
- // Swap with next in selected array
136
- [this.selected[idx], this.selected[idx + 1]] = [this.selected[idx + 1], this.selected[idx]];
137
- // Move cursor down to follow the item
138
- this.cursorIndex++;
139
- }
140
- }
141
- //# sourceMappingURL=ordered-multi-select.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"ordered-multi-select.js","sourceRoot":"","sources":["../../src/components/ordered-multi-select.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Ordered multi-select component.\n *\n * Displays a list of options that can be toggled on/off and reordered.\n * Selected items appear at the top with their position number.\n * Value is stored as a comma-separated string of selected IDs in order.\n */\n\nimport { type Component, getKeybindings, matchesKey, truncateToWidth } from \"@earendil-works/pi-tui\";\nimport type { OrderedListOption } from \"../settings/types.js\";\nimport type { SettingsListTheme } from \"./settings-list.js\";\n\ninterface DisplayItem {\n\toption: OrderedListOption;\n\tselected: boolean;\n\t/** 1-based position within selected items, or 0 if not selected */\n\tposition: number;\n}\n\nexport class OrderedMultiSelect implements Component {\n\tprivate options: OrderedListOption[];\n\tprivate selected: string[];\n\tprivate cursorIndex = 0;\n\tprivate theme: SettingsListTheme;\n\tprivate done: (selectedValue?: string) => void;\n\n\tconstructor(\n\t\toptions: OrderedListOption[],\n\t\tcurrentValue: string,\n\t\ttheme: SettingsListTheme,\n\t\tdone: (selectedValue?: string) => void,\n\t) {\n\t\tthis.options = options;\n\t\tthis.selected = currentValue\n\t\t\t.split(\",\")\n\t\t\t.map((s) => s.trim())\n\t\t\t.filter(Boolean);\n\t\tthis.theme = theme;\n\t\tthis.done = done;\n\t}\n\n\tinvalidate(): void {}\n\n\trender(width: number): string[] {\n\t\tconst lines: string[] = [];\n\t\tconst items = this.buildDisplayItems();\n\n\t\tfor (let i = 0; i < items.length; i++) {\n\t\t\tconst item = items[i];\n\t\t\tconst isCursor = i === this.cursorIndex;\n\t\t\tconst prefix = isCursor ? this.theme.cursor : \" \";\n\n\t\t\tlet marker: string;\n\t\t\tif (item.selected) {\n\t\t\t\tmarker = this.theme.value(`✓ ${String(item.position).padStart(2)}`, isCursor);\n\t\t\t} else {\n\t\t\t\tmarker = this.theme.label(\" \", isCursor);\n\t\t\t}\n\n\t\t\tconst label = this.theme.label(item.option.label, isCursor);\n\t\t\tconst line = `${prefix}${marker} ${label}`;\n\t\t\tlines.push(truncateToWidth(line, width, \"…\"));\n\t\t}\n\n\t\t// Scroll indicator\n\t\tif (items.length > 0) {\n\t\t\tconst scrollText = ` (${this.cursorIndex + 1}/${items.length})`;\n\t\t\tlines.push(this.theme.hint(scrollText));\n\t\t}\n\n\t\t// Hint line\n\t\tlines.push(\"\");\n\t\tlines.push(this.theme.hint(\" Space toggle · Shift+↑/↓ reorder · Enter confirm · Esc cancel\"));\n\n\t\treturn lines;\n\t}\n\n\thandleInput(data: string): void {\n\t\tconst kb = getKeybindings();\n\t\tconst items = this.buildDisplayItems();\n\t\tif (items.length === 0) {\n\t\t\tif (kb.matches(data, \"tui.select.cancel\")) {\n\t\t\t\tthis.done(undefined);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tif (kb.matches(data, \"tui.select.up\") || matchesKey(data, \"up\")) {\n\t\t\tthis.cursorIndex = this.cursorIndex === 0 ? items.length - 1 : this.cursorIndex - 1;\n\t\t} else if (kb.matches(data, \"tui.select.down\") || matchesKey(data, \"down\")) {\n\t\t\tthis.cursorIndex = this.cursorIndex === items.length - 1 ? 0 : this.cursorIndex + 1;\n\t\t} else if (data === \" \" || matchesKey(data, \"space\")) {\n\t\t\tthis.toggleCurrent(items);\n\t\t} else if (matchesKey(data, \"shift+up\")) {\n\t\t\tthis.moveUp(items);\n\t\t} else if (matchesKey(data, \"shift+down\")) {\n\t\t\tthis.moveDown(items);\n\t\t} else if (kb.matches(data, \"tui.select.confirm\")) {\n\t\t\tthis.done(this.selected.join(\",\"));\n\t\t} else if (kb.matches(data, \"tui.select.cancel\")) {\n\t\t\tthis.done(undefined);\n\t\t}\n\t}\n\n\tprivate buildDisplayItems(): DisplayItem[] {\n\t\tconst items: DisplayItem[] = [];\n\n\t\t// Selected items first, in order\n\t\tfor (let i = 0; i < this.selected.length; i++) {\n\t\t\tconst id = this.selected[i];\n\t\t\tconst option = this.options.find((o) => o.id === id);\n\t\t\tif (option) {\n\t\t\t\titems.push({ option, selected: true, position: i + 1 });\n\t\t\t}\n\t\t}\n\n\t\t// Unselected items, in original options order\n\t\tfor (const option of this.options) {\n\t\t\tif (!this.selected.includes(option.id)) {\n\t\t\t\titems.push({ option, selected: false, position: 0 });\n\t\t\t}\n\t\t}\n\n\t\treturn items;\n\t}\n\n\tprivate toggleCurrent(items: DisplayItem[]): void {\n\t\tconst item = items[this.cursorIndex];\n\t\tif (!item) return;\n\n\t\tconst id = item.option.id;\n\t\tif (item.selected) {\n\t\t\t// Remove from selected\n\t\t\tthis.selected = this.selected.filter((s) => s !== id);\n\t\t} else {\n\t\t\t// Add to end of selected\n\t\t\tthis.selected.push(id);\n\t\t}\n\t}\n\n\tprivate moveUp(items: DisplayItem[]): void {\n\t\tconst item = items[this.cursorIndex];\n\t\tif (!item?.selected) return;\n\n\t\tconst idx = this.selected.indexOf(item.option.id);\n\t\tif (idx <= 0) return;\n\n\t\t// Swap with previous in selected array\n\t\t[this.selected[idx - 1], this.selected[idx]] = [this.selected[idx], this.selected[idx - 1]];\n\t\t// Move cursor up to follow the item\n\t\tthis.cursorIndex--;\n\t}\n\n\tprivate moveDown(items: DisplayItem[]): void {\n\t\tconst item = items[this.cursorIndex];\n\t\tif (!item?.selected) return;\n\n\t\tconst idx = this.selected.indexOf(item.option.id);\n\t\tif (idx < 0 || idx >= this.selected.length - 1) return;\n\n\t\t// Swap with next in selected array\n\t\t[this.selected[idx], this.selected[idx + 1]] = [this.selected[idx + 1], this.selected[idx]];\n\t\t// Move cursor down to follow the item\n\t\tthis.cursorIndex++;\n\t}\n}\n"]}
@@ -1,64 +0,0 @@
1
- /**
2
- * Settings list component with support for cycling values and string input.
3
- * Based on @earendil-works/pi-tui SettingsList, extended with string editing.
4
- */
5
- import { type Component } from "@earendil-works/pi-tui";
6
- export interface SettingItem {
7
- /** Unique identifier for this setting */
8
- id: string;
9
- /** Display label (left side) */
10
- label: string;
11
- /** Optional description shown when selected */
12
- description?: string;
13
- /** Current value to display (right side) */
14
- currentValue: string;
15
- /**
16
- * If provided, Enter/Space cycles through these values.
17
- * If undefined/empty, the setting is treated as a string input.
18
- */
19
- values?: string[];
20
- /** If provided, Enter opens this submenu. Receives current value and done callback. */
21
- submenu?: (currentValue: string, done: (selectedValue?: string) => void) => Component;
22
- /** If false, item is selectable but cannot be edited or changed. */
23
- editable?: boolean;
24
- }
25
- export interface SettingsListTheme {
26
- label: (text: string, selected: boolean) => string;
27
- value: (text: string, selected: boolean) => string;
28
- description: (text: string) => string;
29
- cursor: string;
30
- hint: (text: string) => string;
31
- }
32
- export interface SettingsListOptions {
33
- enableSearch?: boolean;
34
- }
35
- export declare class SettingsList implements Component {
36
- private items;
37
- private filteredItems;
38
- private theme;
39
- private selectedIndex;
40
- private maxVisible;
41
- private onChange;
42
- private onCancel;
43
- private searchInput?;
44
- private searchEnabled;
45
- private submenuComponent;
46
- private submenuItemIndex;
47
- private editingInput;
48
- private editingItemIndex;
49
- constructor(items: SettingItem[], maxVisible: number, theme: SettingsListTheme, onChange: (id: string, newValue: string) => void, onCancel: () => void, options?: SettingsListOptions);
50
- /** Update an item's currentValue */
51
- updateValue(id: string, newValue: string): void;
52
- invalidate(): void;
53
- render(width: number): string[];
54
- private renderMainList;
55
- handleInput(data: string): void;
56
- private handleEditingInput;
57
- private confirmEdit;
58
- private cancelEdit;
59
- private activateItem;
60
- private closeSubmenu;
61
- private applyFilter;
62
- private addHintLine;
63
- }
64
- //# sourceMappingURL=settings-list.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"settings-list.d.ts","sourceRoot":"","sources":["../../src/components/settings-list.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EACN,KAAK,SAAS,EAQd,MAAM,wBAAwB,CAAC;AAEhC,MAAM,WAAW,WAAW;IAC3B,yCAAyC;IACzC,EAAE,EAAE,MAAM,CAAC;IACX,gCAAgC;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,+CAA+C;IAC/C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,4CAA4C;IAC5C,YAAY,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,uFAAuF;IACvF,OAAO,CAAC,EAAE,CAAC,YAAY,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,aAAa,CAAC,EAAE,MAAM,KAAK,IAAI,KAAK,SAAS,CAAC;IACtF,oEAAoE;IACpE,QAAQ,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,iBAAiB;IACjC,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,KAAK,MAAM,CAAC;IACnD,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,KAAK,MAAM,CAAC;IACnD,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,mBAAmB;IACnC,YAAY,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,qBAAa,YAAa,YAAW,SAAS;IAC7C,OAAO,CAAC,KAAK,CAAgB;IAC7B,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,KAAK,CAAoB;IACjC,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,QAAQ,CAAyC;IACzD,OAAO,CAAC,QAAQ,CAAa;IAC7B,OAAO,CAAC,WAAW,CAAC,CAAQ;IAC5B,OAAO,CAAC,aAAa,CAAU;IAG/B,OAAO,CAAC,gBAAgB,CAA0B;IAClD,OAAO,CAAC,gBAAgB,CAAuB;IAG/C,OAAO,CAAC,YAAY,CAAsB;IAC1C,OAAO,CAAC,gBAAgB,CAAuB;gBAG9C,KAAK,EAAE,WAAW,EAAE,EACpB,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE,iBAAiB,EACxB,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,EAChD,QAAQ,EAAE,MAAM,IAAI,EACpB,OAAO,GAAE,mBAAwB;IAclC,oCAAoC;IACpC,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI;IAO/C,UAAU,IAAI,IAAI;IAIlB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE;IAS/B,OAAO,CAAC,cAAc;IAqFtB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAqC/B,OAAO,CAAC,kBAAkB;IAmB1B,OAAO,CAAC,WAAW;IAenB,OAAO,CAAC,UAAU;IAKlB,OAAO,CAAC,YAAY;IAkCpB,OAAO,CAAC,YAAY;IASpB,OAAO,CAAC,WAAW;IAKnB,OAAO,CAAC,WAAW;CAUnB"}
@@ -1,251 +0,0 @@
1
- /**
2
- * Settings list component with support for cycling values and string input.
3
- * Based on @earendil-works/pi-tui SettingsList, extended with string editing.
4
- */
5
- import { fuzzyFilter, getKeybindings, Input, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi, } from "@earendil-works/pi-tui";
6
- export class SettingsList {
7
- items;
8
- filteredItems;
9
- theme;
10
- selectedIndex = 0;
11
- maxVisible;
12
- onChange;
13
- onCancel;
14
- searchInput;
15
- searchEnabled;
16
- // Submenu state
17
- submenuComponent = null;
18
- submenuItemIndex = null;
19
- // String editing state
20
- editingInput = null;
21
- editingItemIndex = null;
22
- constructor(items, maxVisible, theme, onChange, onCancel, options = {}) {
23
- this.items = items;
24
- this.filteredItems = items;
25
- this.maxVisible = maxVisible;
26
- this.theme = theme;
27
- this.onChange = onChange;
28
- this.onCancel = onCancel;
29
- this.searchEnabled = options.enableSearch ?? false;
30
- if (this.searchEnabled) {
31
- this.searchInput = new Input();
32
- }
33
- }
34
- /** Update an item's currentValue */
35
- updateValue(id, newValue) {
36
- const item = this.items.find((i) => i.id === id);
37
- if (item) {
38
- item.currentValue = newValue;
39
- }
40
- }
41
- invalidate() {
42
- this.submenuComponent?.invalidate?.();
43
- }
44
- render(width) {
45
- // If submenu is active, render it instead
46
- if (this.submenuComponent) {
47
- return this.submenuComponent.render(width);
48
- }
49
- return this.renderMainList(width);
50
- }
51
- renderMainList(width) {
52
- const lines = [];
53
- if (this.searchEnabled && this.searchInput && !this.editingInput) {
54
- lines.push(...this.searchInput.render(width));
55
- lines.push("");
56
- }
57
- if (this.items.length === 0) {
58
- lines.push(this.theme.hint(" No settings available"));
59
- if (this.searchEnabled) {
60
- this.addHintLine(lines);
61
- }
62
- return lines;
63
- }
64
- const displayItems = this.searchEnabled ? this.filteredItems : this.items;
65
- if (displayItems.length === 0) {
66
- lines.push(this.theme.hint(" No matching settings"));
67
- this.addHintLine(lines);
68
- return lines;
69
- }
70
- // Calculate visible range with scrolling
71
- const startIndex = Math.max(0, Math.min(this.selectedIndex - Math.floor(this.maxVisible / 2), displayItems.length - this.maxVisible));
72
- const endIndex = Math.min(startIndex + this.maxVisible, displayItems.length);
73
- // Calculate max label width for alignment
74
- const maxLabelWidth = Math.min(30, Math.max(...this.items.map((item) => visibleWidth(item.label))));
75
- // Render visible items
76
- for (let i = startIndex; i < endIndex; i++) {
77
- const item = displayItems[i];
78
- if (!item)
79
- continue;
80
- const isSelected = i === this.selectedIndex;
81
- const isEditing = this.editingInput && this.editingItemIndex === i;
82
- const prefix = isSelected ? this.theme.cursor : " ";
83
- const prefixWidth = visibleWidth(prefix);
84
- // Pad label to align values
85
- const labelPadded = item.label + " ".repeat(Math.max(0, maxLabelWidth - visibleWidth(item.label)));
86
- const labelText = this.theme.label(labelPadded, isSelected);
87
- // Calculate space for value
88
- const separator = " ";
89
- const usedWidth = prefixWidth + maxLabelWidth + visibleWidth(separator);
90
- const valueMaxWidth = width - usedWidth - 2;
91
- if (isEditing && this.editingInput) {
92
- // Render inline input for string editing
93
- const inputLines = this.editingInput.render(valueMaxWidth);
94
- const inputLine = inputLines[0] ?? "";
95
- lines.push(prefix + labelText + separator + inputLine);
96
- }
97
- else {
98
- const valueText = this.theme.value(truncateToWidth(item.currentValue, valueMaxWidth, ""), isSelected);
99
- lines.push(prefix + labelText + separator + valueText);
100
- }
101
- }
102
- // Add scroll indicator if needed
103
- if (startIndex > 0 || endIndex < displayItems.length) {
104
- const scrollText = ` (${this.selectedIndex + 1}/${displayItems.length})`;
105
- lines.push(this.theme.hint(truncateToWidth(scrollText, width - 2, "")));
106
- }
107
- // Add description for selected item
108
- const selectedItem = displayItems[this.selectedIndex];
109
- if (selectedItem?.description && !this.editingInput) {
110
- lines.push("");
111
- const wrappedDesc = wrapTextWithAnsi(selectedItem.description, width - 4);
112
- for (const line of wrappedDesc) {
113
- lines.push(this.theme.description(` ${line}`));
114
- }
115
- }
116
- // Add hint
117
- this.addHintLine(lines);
118
- return lines;
119
- }
120
- handleInput(data) {
121
- // If editing a string value, handle input for the editor
122
- if (this.editingInput) {
123
- this.handleEditingInput(data);
124
- return;
125
- }
126
- // If submenu is active, delegate all input to it
127
- if (this.submenuComponent) {
128
- this.submenuComponent.handleInput?.(data);
129
- return;
130
- }
131
- // Main list input handling
132
- const kb = getKeybindings();
133
- const displayItems = this.searchEnabled ? this.filteredItems : this.items;
134
- if (kb.matches(data, "tui.select.up")) {
135
- if (displayItems.length === 0)
136
- return;
137
- this.selectedIndex = this.selectedIndex === 0 ? displayItems.length - 1 : this.selectedIndex - 1;
138
- }
139
- else if (kb.matches(data, "tui.select.down")) {
140
- if (displayItems.length === 0)
141
- return;
142
- this.selectedIndex = this.selectedIndex === displayItems.length - 1 ? 0 : this.selectedIndex + 1;
143
- }
144
- else if (kb.matches(data, "tui.select.confirm") || data === " ") {
145
- this.activateItem();
146
- }
147
- else if (kb.matches(data, "tui.select.cancel")) {
148
- this.onCancel();
149
- }
150
- else if (this.searchEnabled && this.searchInput) {
151
- const sanitized = data.replace(/ /g, "");
152
- if (!sanitized) {
153
- return;
154
- }
155
- this.searchInput.handleInput(sanitized);
156
- this.applyFilter(this.searchInput.getValue());
157
- }
158
- }
159
- handleEditingInput(data) {
160
- if (!this.editingInput)
161
- return;
162
- // Enter: confirm edit
163
- if (matchesKey(data, "enter")) {
164
- this.confirmEdit();
165
- return;
166
- }
167
- // Escape: cancel edit
168
- if (matchesKey(data, "escape")) {
169
- this.cancelEdit();
170
- return;
171
- }
172
- // Pass other input to the editor
173
- this.editingInput.handleInput(data);
174
- }
175
- confirmEdit() {
176
- if (!this.editingInput || this.editingItemIndex === null)
177
- return;
178
- const displayItems = this.searchEnabled ? this.filteredItems : this.items;
179
- const item = displayItems[this.editingItemIndex];
180
- if (item) {
181
- const newValue = this.editingInput.getValue();
182
- item.currentValue = newValue;
183
- this.onChange(item.id, newValue);
184
- }
185
- this.editingInput = null;
186
- this.editingItemIndex = null;
187
- }
188
- cancelEdit() {
189
- this.editingInput = null;
190
- this.editingItemIndex = null;
191
- }
192
- activateItem() {
193
- const displayItems = this.searchEnabled ? this.filteredItems : this.items;
194
- const item = displayItems[this.selectedIndex];
195
- if (!item)
196
- return;
197
- if (item.editable === false) {
198
- return;
199
- }
200
- if (item.submenu) {
201
- // Open submenu, passing current value so it can pre-select correctly
202
- this.submenuItemIndex = this.selectedIndex;
203
- this.submenuComponent = item.submenu(item.currentValue, (selectedValue) => {
204
- if (selectedValue !== undefined) {
205
- item.currentValue = selectedValue;
206
- this.onChange(item.id, selectedValue);
207
- }
208
- this.closeSubmenu();
209
- });
210
- }
211
- else if (item.values && item.values.length > 0) {
212
- // Cycle through values
213
- const currentIndex = item.values.indexOf(item.currentValue);
214
- const nextIndex = (currentIndex + 1) % item.values.length;
215
- const newValue = item.values[nextIndex];
216
- item.currentValue = newValue;
217
- this.onChange(item.id, newValue);
218
- }
219
- else {
220
- // String input - start editing
221
- this.editingItemIndex = this.selectedIndex;
222
- this.editingInput = new Input();
223
- this.editingInput.setValue(item.currentValue);
224
- }
225
- }
226
- closeSubmenu() {
227
- this.submenuComponent = null;
228
- // Restore selection to the item that opened the submenu
229
- if (this.submenuItemIndex !== null) {
230
- this.selectedIndex = this.submenuItemIndex;
231
- this.submenuItemIndex = null;
232
- }
233
- }
234
- applyFilter(query) {
235
- this.filteredItems = fuzzyFilter(this.items, query, (item) => item.label);
236
- this.selectedIndex = 0;
237
- }
238
- addHintLine(lines) {
239
- lines.push("");
240
- if (this.editingInput) {
241
- lines.push(this.theme.hint(" Enter to confirm · Esc to cancel"));
242
- }
243
- else if (this.searchEnabled) {
244
- lines.push(this.theme.hint(" Type to search · Enter/Space to change · Esc to cancel"));
245
- }
246
- else {
247
- lines.push(this.theme.hint(" Enter/Space to change · Esc to cancel"));
248
- }
249
- }
250
- }
251
- //# sourceMappingURL=settings-list.js.map