@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.
- package/package.json +4 -14
- package/src/components/ordered-multi-select.ts +166 -0
- package/src/components/settings-list.ts +328 -0
- package/src/extension.ts +125 -0
- package/{dist/index.d.ts → src/index.ts} +5 -1
- package/src/settings/storage.ts +86 -0
- package/src/settings/types.ts +34 -0
- package/dist/components/ordered-multi-select.d.ts +0 -26
- package/dist/components/ordered-multi-select.d.ts.map +0 -1
- package/dist/components/ordered-multi-select.js +0 -141
- package/dist/components/ordered-multi-select.js.map +0 -1
- package/dist/components/settings-list.d.ts +0 -64
- package/dist/components/settings-list.d.ts.map +0 -1
- package/dist/components/settings-list.js +0 -251
- package/dist/components/settings-list.js.map +0 -1
- package/dist/extension.d.ts +0 -6
- package/dist/extension.d.ts.map +0 -1
- package/dist/extension.js +0 -96
- package/dist/extension.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -40
- package/dist/index.js.map +0 -1
- package/dist/settings/storage.d.ts +0 -23
- package/dist/settings/storage.d.ts.map +0 -1
- package/dist/settings/storage.js +0 -75
- package/dist/settings/storage.js.map +0 -1
- package/dist/settings/types.d.ts +0 -33
- package/dist/settings/types.d.ts.map +0 -1
- package/dist/settings/types.js +0 -5
- package/dist/settings/types.js.map +0 -1
|
@@ -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
|