@greppleai/grip-tui 0.0.0-stage → 0.0.1
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/README.md +818 -2
- package/dist/autocomplete.d.ts +48 -0
- package/dist/autocomplete.d.ts.map +1 -0
- package/dist/autocomplete.js +566 -0
- package/dist/autocomplete.js.map +1 -0
- package/dist/components/alt-screen-flash.d.ts +13 -0
- package/dist/components/alt-screen-flash.d.ts.map +1 -0
- package/dist/components/alt-screen-flash.js +37 -0
- package/dist/components/alt-screen-flash.js.map +1 -0
- package/dist/components/box.d.ts +22 -0
- package/dist/components/box.d.ts.map +1 -0
- package/dist/components/box.js +104 -0
- package/dist/components/box.js.map +1 -0
- package/dist/components/cancellable-loader.d.ts +22 -0
- package/dist/components/cancellable-loader.d.ts.map +1 -0
- package/dist/components/cancellable-loader.js +35 -0
- package/dist/components/cancellable-loader.js.map +1 -0
- package/dist/components/editor.d.ts +292 -0
- package/dist/components/editor.d.ts.map +1 -0
- package/dist/components/editor.js +1887 -0
- package/dist/components/editor.js.map +1 -0
- package/dist/components/h-stack.d.ts +7 -0
- package/dist/components/h-stack.d.ts.map +1 -0
- package/dist/components/h-stack.js +43 -0
- package/dist/components/h-stack.js.map +1 -0
- package/dist/components/image.d.ts +32 -0
- package/dist/components/image.d.ts.map +1 -0
- package/dist/components/image.js +76 -0
- package/dist/components/image.js.map +1 -0
- package/dist/components/input.d.ts +46 -0
- package/dist/components/input.d.ts.map +1 -0
- package/dist/components/input.js +352 -0
- package/dist/components/input.js.map +1 -0
- package/dist/components/loader.d.ts +32 -0
- package/dist/components/loader.d.ts.map +1 -0
- package/dist/components/loader.js +80 -0
- package/dist/components/loader.js.map +1 -0
- package/dist/components/markdown.d.ts +132 -0
- package/dist/components/markdown.d.ts.map +1 -0
- package/dist/components/markdown.js +717 -0
- package/dist/components/markdown.js.map +1 -0
- package/dist/components/scroll-view.d.ts +53 -0
- package/dist/components/scroll-view.d.ts.map +1 -0
- package/dist/components/scroll-view.js +173 -0
- package/dist/components/scroll-view.js.map +1 -0
- package/dist/components/select-list.d.ts +50 -0
- package/dist/components/select-list.d.ts.map +1 -0
- package/dist/components/select-list.js +159 -0
- package/dist/components/select-list.js.map +1 -0
- package/dist/components/settings-list.d.ts +52 -0
- package/dist/components/settings-list.d.ts.map +1 -0
- package/dist/components/settings-list.js +187 -0
- package/dist/components/settings-list.js.map +1 -0
- package/dist/components/spacer.d.ts +12 -0
- package/dist/components/spacer.d.ts.map +1 -0
- package/dist/components/spacer.js +23 -0
- package/dist/components/spacer.js.map +1 -0
- package/dist/components/stack.d.ts +32 -0
- package/dist/components/stack.d.ts.map +1 -0
- package/dist/components/stack.js +122 -0
- package/dist/components/stack.js.map +1 -0
- package/dist/components/text.d.ts +19 -0
- package/dist/components/text.d.ts.map +1 -0
- package/dist/components/text.js +89 -0
- package/dist/components/text.js.map +1 -0
- package/dist/components/truncated-text.d.ts +13 -0
- package/dist/components/truncated-text.d.ts.map +1 -0
- package/dist/components/truncated-text.js +51 -0
- package/dist/components/truncated-text.js.map +1 -0
- package/dist/components/v-stack.d.ts +8 -0
- package/dist/components/v-stack.d.ts.map +1 -0
- package/dist/components/v-stack.js +27 -0
- package/dist/components/v-stack.js.map +1 -0
- package/dist/editor-component.d.ts +39 -0
- package/dist/editor-component.d.ts.map +1 -0
- package/dist/editor-component.js +2 -0
- package/dist/editor-component.js.map +1 -0
- package/dist/fuzzy.d.ts +16 -0
- package/dist/fuzzy.d.ts.map +1 -0
- package/dist/fuzzy.js +101 -0
- package/dist/fuzzy.js.map +1 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +41 -0
- package/dist/index.js.map +1 -0
- package/dist/keybindings.d.ts +243 -0
- package/dist/keybindings.d.ts.map +1 -0
- package/dist/keybindings.js +209 -0
- package/dist/keybindings.js.map +1 -0
- package/dist/keys.d.ts +184 -0
- package/dist/keys.d.ts.map +1 -0
- package/dist/keys.js +1053 -0
- package/dist/keys.js.map +1 -0
- package/dist/kill-ring.d.ts +30 -0
- package/dist/kill-ring.d.ts.map +1 -0
- package/dist/kill-ring.js +37 -0
- package/dist/kill-ring.js.map +1 -0
- package/dist/latex.d.ts +10 -0
- package/dist/latex.d.ts.map +1 -0
- package/dist/latex.js +1406 -0
- package/dist/latex.js.map +1 -0
- package/dist/layout-node.d.ts +43 -0
- package/dist/layout-node.d.ts.map +1 -0
- package/dist/layout-node.js +6 -0
- package/dist/layout-node.js.map +1 -0
- package/dist/layout.d.ts +40 -0
- package/dist/layout.d.ts.map +1 -0
- package/dist/layout.js +386 -0
- package/dist/layout.js.map +1 -0
- package/dist/native-modifiers.d.ts +3 -0
- package/dist/native-modifiers.d.ts.map +1 -0
- package/dist/native-modifiers.js +53 -0
- package/dist/native-modifiers.js.map +1 -0
- package/dist/stdin-buffer.d.ts +58 -0
- package/dist/stdin-buffer.d.ts.map +1 -0
- package/dist/stdin-buffer.js +318 -0
- package/dist/stdin-buffer.js.map +1 -0
- package/dist/terminal-control.d.ts +5 -0
- package/dist/terminal-control.d.ts.map +1 -0
- package/dist/terminal-control.js +26 -0
- package/dist/terminal-control.js.map +1 -0
- package/dist/terminal-image.d.ts +112 -0
- package/dist/terminal-image.d.ts.map +1 -0
- package/dist/terminal-image.js +508 -0
- package/dist/terminal-image.js.map +1 -0
- package/dist/terminal.d.ts +117 -0
- package/dist/terminal.d.ts.map +1 -0
- package/dist/terminal.js +490 -0
- package/dist/terminal.js.map +1 -0
- package/dist/tui-alt-screen.d.ts +114 -0
- package/dist/tui-alt-screen.d.ts.map +1 -0
- package/dist/tui-alt-screen.js +950 -0
- package/dist/tui-alt-screen.js.map +1 -0
- package/dist/tui-main-screen.d.ts +52 -0
- package/dist/tui-main-screen.d.ts.map +1 -0
- package/dist/tui-main-screen.js +511 -0
- package/dist/tui-main-screen.js.map +1 -0
- package/dist/tui.d.ts +295 -0
- package/dist/tui.d.ts.map +1 -0
- package/dist/tui.js +868 -0
- package/dist/tui.js.map +1 -0
- package/dist/undo-stack.d.ts +17 -0
- package/dist/undo-stack.d.ts.map +1 -0
- package/dist/undo-stack.js +25 -0
- package/dist/undo-stack.js.map +1 -0
- package/dist/utils.d.ts +96 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +1106 -0
- package/dist/utils.js.map +1 -0
- package/dist/word-navigation.d.ts +25 -0
- package/dist/word-navigation.d.ts.map +1 -0
- package/dist/word-navigation.js +94 -0
- package/dist/word-navigation.js.map +1 -0
- package/native/darwin/prebuilds/darwin-arm64/darwin-modifiers.node +0 -0
- package/native/darwin/prebuilds/darwin-x64/darwin-modifiers.node +0 -0
- package/native/win32/prebuilds/win32-arm64/win32-console-mode.node +0 -0
- package/native/win32/prebuilds/win32-x64/win32-console-mode.node +0 -0
- package/package.json +47 -5
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { getKeybindings } from "../keybindings.js";
|
|
2
|
+
import { truncateToWidth, visibleWidth } from "../utils.js";
|
|
3
|
+
const DEFAULT_PRIMARY_COLUMN_WIDTH = 32;
|
|
4
|
+
const PRIMARY_COLUMN_GAP = 2;
|
|
5
|
+
const MIN_DESCRIPTION_WIDTH = 10;
|
|
6
|
+
const normalizeToSingleLine = (text) => text.replace(/[\r\n]+/g, " ").trim();
|
|
7
|
+
const clamp = (value, min, max) => Math.max(min, Math.min(value, max));
|
|
8
|
+
export class SelectList {
|
|
9
|
+
items = [];
|
|
10
|
+
filteredItems = [];
|
|
11
|
+
selectedIndex = 0;
|
|
12
|
+
maxVisible = 5;
|
|
13
|
+
theme;
|
|
14
|
+
layout;
|
|
15
|
+
onSelect;
|
|
16
|
+
onCancel;
|
|
17
|
+
onSelectionChange;
|
|
18
|
+
constructor(items, maxVisible, theme, layout = {}) {
|
|
19
|
+
this.items = items;
|
|
20
|
+
this.filteredItems = items;
|
|
21
|
+
this.maxVisible = maxVisible;
|
|
22
|
+
this.theme = theme;
|
|
23
|
+
this.layout = layout;
|
|
24
|
+
}
|
|
25
|
+
setFilter(filter) {
|
|
26
|
+
this.filteredItems = this.items.filter((item) => item.value.toLowerCase().startsWith(filter.toLowerCase()));
|
|
27
|
+
// Reset selection when filter changes
|
|
28
|
+
this.selectedIndex = 0;
|
|
29
|
+
}
|
|
30
|
+
setSelectedIndex(index) {
|
|
31
|
+
this.selectedIndex = Math.max(0, Math.min(index, this.filteredItems.length - 1));
|
|
32
|
+
}
|
|
33
|
+
invalidate() {
|
|
34
|
+
// No cached state to invalidate currently
|
|
35
|
+
}
|
|
36
|
+
render(width) {
|
|
37
|
+
const lines = [];
|
|
38
|
+
// If no items match filter, show message
|
|
39
|
+
if (this.filteredItems.length === 0) {
|
|
40
|
+
lines.push(this.theme.noMatch(" No matching commands"));
|
|
41
|
+
return lines;
|
|
42
|
+
}
|
|
43
|
+
const primaryColumnWidth = this.getPrimaryColumnWidth();
|
|
44
|
+
// Calculate visible range with scrolling
|
|
45
|
+
const startIndex = Math.max(0, Math.min(this.selectedIndex - Math.floor(this.maxVisible / 2), this.filteredItems.length - this.maxVisible));
|
|
46
|
+
const endIndex = Math.min(startIndex + this.maxVisible, this.filteredItems.length);
|
|
47
|
+
// Render visible items
|
|
48
|
+
for (let i = startIndex; i < endIndex; i++) {
|
|
49
|
+
const item = this.filteredItems[i];
|
|
50
|
+
if (!item)
|
|
51
|
+
continue;
|
|
52
|
+
const isSelected = i === this.selectedIndex;
|
|
53
|
+
const descriptionSingleLine = item.description ? normalizeToSingleLine(item.description) : undefined;
|
|
54
|
+
lines.push(this.renderItem(item, isSelected, width, descriptionSingleLine, primaryColumnWidth));
|
|
55
|
+
}
|
|
56
|
+
// Add scroll indicators if needed
|
|
57
|
+
if (startIndex > 0 || endIndex < this.filteredItems.length) {
|
|
58
|
+
const scrollText = ` (${this.selectedIndex + 1}/${this.filteredItems.length})`;
|
|
59
|
+
// Truncate if too long for terminal
|
|
60
|
+
lines.push(this.theme.scrollInfo(truncateToWidth(scrollText, width - 2, "")));
|
|
61
|
+
}
|
|
62
|
+
return lines;
|
|
63
|
+
}
|
|
64
|
+
handleInput(keyData) {
|
|
65
|
+
const kb = getKeybindings();
|
|
66
|
+
// Up arrow - wrap to bottom when at top
|
|
67
|
+
if (kb.matches(keyData, "tui.select.up")) {
|
|
68
|
+
this.selectedIndex = this.selectedIndex === 0 ? this.filteredItems.length - 1 : this.selectedIndex - 1;
|
|
69
|
+
this.notifySelectionChange();
|
|
70
|
+
}
|
|
71
|
+
// Down arrow - wrap to top when at bottom
|
|
72
|
+
else if (kb.matches(keyData, "tui.select.down")) {
|
|
73
|
+
this.selectedIndex = this.selectedIndex === this.filteredItems.length - 1 ? 0 : this.selectedIndex + 1;
|
|
74
|
+
this.notifySelectionChange();
|
|
75
|
+
}
|
|
76
|
+
// Enter
|
|
77
|
+
else if (kb.matches(keyData, "tui.select.confirm")) {
|
|
78
|
+
const selectedItem = this.filteredItems[this.selectedIndex];
|
|
79
|
+
if (selectedItem && this.onSelect) {
|
|
80
|
+
this.onSelect(selectedItem);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// Escape or Ctrl+C
|
|
84
|
+
else if (kb.matches(keyData, "tui.select.cancel")) {
|
|
85
|
+
if (this.onCancel) {
|
|
86
|
+
this.onCancel();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
renderItem(item, isSelected, width, descriptionSingleLine, primaryColumnWidth) {
|
|
91
|
+
const prefix = isSelected ? "→ " : " ";
|
|
92
|
+
const prefixWidth = visibleWidth(prefix);
|
|
93
|
+
if (descriptionSingleLine && width > 40) {
|
|
94
|
+
const effectivePrimaryColumnWidth = Math.max(1, Math.min(primaryColumnWidth, width - prefixWidth - 4));
|
|
95
|
+
const maxPrimaryWidth = Math.max(1, effectivePrimaryColumnWidth - PRIMARY_COLUMN_GAP);
|
|
96
|
+
const truncatedValue = this.truncatePrimary(item, isSelected, maxPrimaryWidth, effectivePrimaryColumnWidth);
|
|
97
|
+
const truncatedValueWidth = visibleWidth(truncatedValue);
|
|
98
|
+
const spacing = " ".repeat(Math.max(1, effectivePrimaryColumnWidth - truncatedValueWidth));
|
|
99
|
+
const descriptionStart = prefixWidth + truncatedValueWidth + spacing.length;
|
|
100
|
+
const remainingWidth = width - descriptionStart - 2; // -2 for safety
|
|
101
|
+
if (remainingWidth > MIN_DESCRIPTION_WIDTH) {
|
|
102
|
+
const truncatedDesc = truncateToWidth(descriptionSingleLine, remainingWidth, "");
|
|
103
|
+
if (isSelected) {
|
|
104
|
+
return this.theme.selectedText(`${prefix}${truncatedValue}${spacing}${truncatedDesc}`);
|
|
105
|
+
}
|
|
106
|
+
const descText = this.theme.description(spacing + truncatedDesc);
|
|
107
|
+
return prefix + truncatedValue + descText;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const maxWidth = width - prefixWidth - 2;
|
|
111
|
+
const truncatedValue = this.truncatePrimary(item, isSelected, maxWidth, maxWidth);
|
|
112
|
+
if (isSelected) {
|
|
113
|
+
return this.theme.selectedText(`${prefix}${truncatedValue}`);
|
|
114
|
+
}
|
|
115
|
+
return prefix + truncatedValue;
|
|
116
|
+
}
|
|
117
|
+
getPrimaryColumnWidth() {
|
|
118
|
+
const { min, max } = this.getPrimaryColumnBounds();
|
|
119
|
+
const widestPrimary = this.filteredItems.reduce((widest, item) => {
|
|
120
|
+
return Math.max(widest, visibleWidth(this.getDisplayValue(item)) + PRIMARY_COLUMN_GAP);
|
|
121
|
+
}, 0);
|
|
122
|
+
return clamp(widestPrimary, min, max);
|
|
123
|
+
}
|
|
124
|
+
getPrimaryColumnBounds() {
|
|
125
|
+
const rawMin = this.layout.minPrimaryColumnWidth ?? this.layout.maxPrimaryColumnWidth ?? DEFAULT_PRIMARY_COLUMN_WIDTH;
|
|
126
|
+
const rawMax = this.layout.maxPrimaryColumnWidth ?? this.layout.minPrimaryColumnWidth ?? DEFAULT_PRIMARY_COLUMN_WIDTH;
|
|
127
|
+
return {
|
|
128
|
+
min: Math.max(1, Math.min(rawMin, rawMax)),
|
|
129
|
+
max: Math.max(1, Math.max(rawMin, rawMax)),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
truncatePrimary(item, isSelected, maxWidth, columnWidth) {
|
|
133
|
+
const displayValue = this.getDisplayValue(item);
|
|
134
|
+
const truncatedValue = this.layout.truncatePrimary
|
|
135
|
+
? this.layout.truncatePrimary({
|
|
136
|
+
text: displayValue,
|
|
137
|
+
maxWidth,
|
|
138
|
+
columnWidth,
|
|
139
|
+
item,
|
|
140
|
+
isSelected,
|
|
141
|
+
})
|
|
142
|
+
: truncateToWidth(displayValue, maxWidth, "");
|
|
143
|
+
return truncateToWidth(truncatedValue, maxWidth, "");
|
|
144
|
+
}
|
|
145
|
+
getDisplayValue(item) {
|
|
146
|
+
return item.label || item.value;
|
|
147
|
+
}
|
|
148
|
+
notifySelectionChange() {
|
|
149
|
+
const selectedItem = this.filteredItems[this.selectedIndex];
|
|
150
|
+
if (selectedItem && this.onSelectionChange) {
|
|
151
|
+
this.onSelectionChange(selectedItem);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
getSelectedItem() {
|
|
155
|
+
const item = this.filteredItems[this.selectedIndex];
|
|
156
|
+
return item || null;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
//# sourceMappingURL=select-list.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"select-list.js","sourceRoot":"","sources":["../../src/components/select-list.ts"],"names":[],"mappings":"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","sourcesContent":["import { getKeybindings } from \"../keybindings.ts\";\nimport type { Component } from \"../tui.ts\";\nimport { truncateToWidth, visibleWidth } from \"../utils.ts\";\n\nconst DEFAULT_PRIMARY_COLUMN_WIDTH = 32;\nconst PRIMARY_COLUMN_GAP = 2;\nconst MIN_DESCRIPTION_WIDTH = 10;\n\nconst normalizeToSingleLine = (text: string): string => text.replace(/[\\r\\n]+/g, \" \").trim();\nconst clamp = (value: number, min: number, max: number): number => Math.max(min, Math.min(value, max));\n\nexport interface SelectItem {\n\tvalue: string;\n\tlabel: string;\n\tdescription?: string;\n}\n\nexport interface SelectListTheme {\n\tselectedPrefix: (text: string) => string;\n\tselectedText: (text: string) => string;\n\tdescription: (text: string) => string;\n\tscrollInfo: (text: string) => string;\n\tnoMatch: (text: string) => string;\n}\n\nexport interface SelectListTruncatePrimaryContext {\n\ttext: string;\n\tmaxWidth: number;\n\tcolumnWidth: number;\n\titem: SelectItem;\n\tisSelected: boolean;\n}\n\nexport interface SelectListLayoutOptions {\n\tminPrimaryColumnWidth?: number;\n\tmaxPrimaryColumnWidth?: number;\n\ttruncatePrimary?: (context: SelectListTruncatePrimaryContext) => string;\n}\n\ninterface SelectListPrimaryColumnBounds {\n\tmin: number;\n\tmax: number;\n}\n\nexport class SelectList implements Component {\n\tprivate items: SelectItem[] = [];\n\tprivate filteredItems: SelectItem[] = [];\n\tprivate selectedIndex: number = 0;\n\tprivate maxVisible: number = 5;\n\tprivate theme: SelectListTheme;\n\tprivate layout: SelectListLayoutOptions;\n\n\tpublic onSelect?: (item: SelectItem) => void;\n\tpublic onCancel?: () => void;\n\tpublic onSelectionChange?: (item: SelectItem) => void;\n\n\tconstructor(items: SelectItem[], maxVisible: number, theme: SelectListTheme, layout: SelectListLayoutOptions = {}) {\n\t\tthis.items = items;\n\t\tthis.filteredItems = items;\n\t\tthis.maxVisible = maxVisible;\n\t\tthis.theme = theme;\n\t\tthis.layout = layout;\n\t}\n\n\tsetFilter(filter: string): void {\n\t\tthis.filteredItems = this.items.filter((item) => item.value.toLowerCase().startsWith(filter.toLowerCase()));\n\t\t// Reset selection when filter changes\n\t\tthis.selectedIndex = 0;\n\t}\n\n\tsetSelectedIndex(index: number): void {\n\t\tthis.selectedIndex = Math.max(0, Math.min(index, this.filteredItems.length - 1));\n\t}\n\n\tinvalidate(): void {\n\t\t// No cached state to invalidate currently\n\t}\n\n\trender(width: number): string[] {\n\t\tconst lines: string[] = [];\n\n\t\t// If no items match filter, show message\n\t\tif (this.filteredItems.length === 0) {\n\t\t\tlines.push(this.theme.noMatch(\" No matching commands\"));\n\t\t\treturn lines;\n\t\t}\n\n\t\tconst primaryColumnWidth = this.getPrimaryColumnWidth();\n\n\t\t// Calculate visible range with scrolling\n\t\tconst startIndex = Math.max(\n\t\t\t0,\n\t\t\tMath.min(this.selectedIndex - Math.floor(this.maxVisible / 2), this.filteredItems.length - this.maxVisible),\n\t\t);\n\t\tconst endIndex = Math.min(startIndex + this.maxVisible, this.filteredItems.length);\n\n\t\t// Render visible items\n\t\tfor (let i = startIndex; i < endIndex; i++) {\n\t\t\tconst item = this.filteredItems[i];\n\t\t\tif (!item) continue;\n\n\t\t\tconst isSelected = i === this.selectedIndex;\n\t\t\tconst descriptionSingleLine = item.description ? normalizeToSingleLine(item.description) : undefined;\n\t\t\tlines.push(this.renderItem(item, isSelected, width, descriptionSingleLine, primaryColumnWidth));\n\t\t}\n\n\t\t// Add scroll indicators if needed\n\t\tif (startIndex > 0 || endIndex < this.filteredItems.length) {\n\t\t\tconst scrollText = ` (${this.selectedIndex + 1}/${this.filteredItems.length})`;\n\t\t\t// Truncate if too long for terminal\n\t\t\tlines.push(this.theme.scrollInfo(truncateToWidth(scrollText, width - 2, \"\")));\n\t\t}\n\n\t\treturn lines;\n\t}\n\n\thandleInput(keyData: string): void {\n\t\tconst kb = getKeybindings();\n\t\t// Up arrow - wrap to bottom when at top\n\t\tif (kb.matches(keyData, \"tui.select.up\")) {\n\t\t\tthis.selectedIndex = this.selectedIndex === 0 ? this.filteredItems.length - 1 : this.selectedIndex - 1;\n\t\t\tthis.notifySelectionChange();\n\t\t}\n\t\t// Down arrow - wrap to top when at bottom\n\t\telse if (kb.matches(keyData, \"tui.select.down\")) {\n\t\t\tthis.selectedIndex = this.selectedIndex === this.filteredItems.length - 1 ? 0 : this.selectedIndex + 1;\n\t\t\tthis.notifySelectionChange();\n\t\t}\n\t\t// Enter\n\t\telse if (kb.matches(keyData, \"tui.select.confirm\")) {\n\t\t\tconst selectedItem = this.filteredItems[this.selectedIndex];\n\t\t\tif (selectedItem && this.onSelect) {\n\t\t\t\tthis.onSelect(selectedItem);\n\t\t\t}\n\t\t}\n\t\t// Escape or Ctrl+C\n\t\telse if (kb.matches(keyData, \"tui.select.cancel\")) {\n\t\t\tif (this.onCancel) {\n\t\t\t\tthis.onCancel();\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate renderItem(\n\t\titem: SelectItem,\n\t\tisSelected: boolean,\n\t\twidth: number,\n\t\tdescriptionSingleLine: string | undefined,\n\t\tprimaryColumnWidth: number,\n\t): string {\n\t\tconst prefix = isSelected ? \"→ \" : \" \";\n\t\tconst prefixWidth = visibleWidth(prefix);\n\n\t\tif (descriptionSingleLine && width > 40) {\n\t\t\tconst effectivePrimaryColumnWidth = Math.max(1, Math.min(primaryColumnWidth, width - prefixWidth - 4));\n\t\t\tconst maxPrimaryWidth = Math.max(1, effectivePrimaryColumnWidth - PRIMARY_COLUMN_GAP);\n\t\t\tconst truncatedValue = this.truncatePrimary(item, isSelected, maxPrimaryWidth, effectivePrimaryColumnWidth);\n\t\t\tconst truncatedValueWidth = visibleWidth(truncatedValue);\n\t\t\tconst spacing = \" \".repeat(Math.max(1, effectivePrimaryColumnWidth - truncatedValueWidth));\n\t\t\tconst descriptionStart = prefixWidth + truncatedValueWidth + spacing.length;\n\t\t\tconst remainingWidth = width - descriptionStart - 2; // -2 for safety\n\n\t\t\tif (remainingWidth > MIN_DESCRIPTION_WIDTH) {\n\t\t\t\tconst truncatedDesc = truncateToWidth(descriptionSingleLine, remainingWidth, \"\");\n\t\t\t\tif (isSelected) {\n\t\t\t\t\treturn this.theme.selectedText(`${prefix}${truncatedValue}${spacing}${truncatedDesc}`);\n\t\t\t\t}\n\n\t\t\t\tconst descText = this.theme.description(spacing + truncatedDesc);\n\t\t\t\treturn prefix + truncatedValue + descText;\n\t\t\t}\n\t\t}\n\n\t\tconst maxWidth = width - prefixWidth - 2;\n\t\tconst truncatedValue = this.truncatePrimary(item, isSelected, maxWidth, maxWidth);\n\t\tif (isSelected) {\n\t\t\treturn this.theme.selectedText(`${prefix}${truncatedValue}`);\n\t\t}\n\n\t\treturn prefix + truncatedValue;\n\t}\n\n\tprivate getPrimaryColumnWidth(): number {\n\t\tconst { min, max } = this.getPrimaryColumnBounds();\n\t\tconst widestPrimary = this.filteredItems.reduce((widest, item) => {\n\t\t\treturn Math.max(widest, visibleWidth(this.getDisplayValue(item)) + PRIMARY_COLUMN_GAP);\n\t\t}, 0);\n\n\t\treturn clamp(widestPrimary, min, max);\n\t}\n\n\tprivate getPrimaryColumnBounds(): SelectListPrimaryColumnBounds {\n\t\tconst rawMin =\n\t\t\tthis.layout.minPrimaryColumnWidth ?? this.layout.maxPrimaryColumnWidth ?? DEFAULT_PRIMARY_COLUMN_WIDTH;\n\t\tconst rawMax =\n\t\t\tthis.layout.maxPrimaryColumnWidth ?? this.layout.minPrimaryColumnWidth ?? DEFAULT_PRIMARY_COLUMN_WIDTH;\n\n\t\treturn {\n\t\t\tmin: Math.max(1, Math.min(rawMin, rawMax)),\n\t\t\tmax: Math.max(1, Math.max(rawMin, rawMax)),\n\t\t};\n\t}\n\n\tprivate truncatePrimary(item: SelectItem, isSelected: boolean, maxWidth: number, columnWidth: number): string {\n\t\tconst displayValue = this.getDisplayValue(item);\n\t\tconst truncatedValue = this.layout.truncatePrimary\n\t\t\t? this.layout.truncatePrimary({\n\t\t\t\t\ttext: displayValue,\n\t\t\t\t\tmaxWidth,\n\t\t\t\t\tcolumnWidth,\n\t\t\t\t\titem,\n\t\t\t\t\tisSelected,\n\t\t\t\t})\n\t\t\t: truncateToWidth(displayValue, maxWidth, \"\");\n\n\t\treturn truncateToWidth(truncatedValue, maxWidth, \"\");\n\t}\n\n\tprivate getDisplayValue(item: SelectItem): string {\n\t\treturn item.label || item.value;\n\t}\n\n\tprivate notifySelectionChange(): void {\n\t\tconst selectedItem = this.filteredItems[this.selectedIndex];\n\t\tif (selectedItem && this.onSelectionChange) {\n\t\t\tthis.onSelectionChange(selectedItem);\n\t\t}\n\t}\n\n\tgetSelectedItem(): SelectItem | null {\n\t\tconst item = this.filteredItems[this.selectedIndex];\n\t\treturn item || null;\n\t}\n}\n"]}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Component } from "../tui.ts";
|
|
2
|
+
export interface SettingItem {
|
|
3
|
+
/** Unique identifier for this setting */
|
|
4
|
+
id: string;
|
|
5
|
+
/** Display label (left side) */
|
|
6
|
+
label: string;
|
|
7
|
+
/** Optional description shown when selected */
|
|
8
|
+
description?: string;
|
|
9
|
+
/** Current value to display (right side) */
|
|
10
|
+
currentValue: string;
|
|
11
|
+
/** If provided, Enter/Space cycles through these values */
|
|
12
|
+
values?: string[];
|
|
13
|
+
/** If provided, Enter opens this submenu. Receives current value and done callback. */
|
|
14
|
+
submenu?: (currentValue: string, done: (selectedValue?: string) => void) => Component;
|
|
15
|
+
}
|
|
16
|
+
export interface SettingsListTheme {
|
|
17
|
+
label: (text: string, selected: boolean) => string;
|
|
18
|
+
value: (text: string, selected: boolean) => string;
|
|
19
|
+
description: (text: string) => string;
|
|
20
|
+
cursor: string;
|
|
21
|
+
hint: (text: string) => string;
|
|
22
|
+
}
|
|
23
|
+
export interface SettingsListOptions {
|
|
24
|
+
enableSearch?: boolean;
|
|
25
|
+
}
|
|
26
|
+
export declare class SettingsList implements Component {
|
|
27
|
+
private items;
|
|
28
|
+
private filteredItems;
|
|
29
|
+
private theme;
|
|
30
|
+
private selectedIndex;
|
|
31
|
+
private maxVisible;
|
|
32
|
+
private onChange;
|
|
33
|
+
private onCancel;
|
|
34
|
+
private searchInput?;
|
|
35
|
+
private searchEnabled;
|
|
36
|
+
private submenuComponent;
|
|
37
|
+
private submenuItemIndex;
|
|
38
|
+
constructor(items: SettingItem[], maxVisible: number, theme: SettingsListTheme, onChange: (id: string, newValue: string) => void, onCancel: () => void, options?: SettingsListOptions);
|
|
39
|
+
/** Update an item's currentValue */
|
|
40
|
+
updateValue(id: string, newValue: string): void;
|
|
41
|
+
invalidate(): void;
|
|
42
|
+
render(width: number): string[];
|
|
43
|
+
private renderMainList;
|
|
44
|
+
private appendSelectedDescription;
|
|
45
|
+
private handleSelectionMovement;
|
|
46
|
+
handleInput(data: string): void;
|
|
47
|
+
private activateItem;
|
|
48
|
+
private closeSubmenu;
|
|
49
|
+
private applyFilter;
|
|
50
|
+
private addHintLine;
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=settings-list.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settings-list.d.ts","sourceRoot":"","sources":["../../src/components/settings-list.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAI3C,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,2DAA2D;IAC3D,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;CACtF;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;IAE/C,YACC,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,EAYjC;IAED,oCAAoC;IACpC,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAK9C;IAED,UAAU,IAAI,IAAI,CAEjB;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAO9B;IAED,OAAO,CAAC,cAAc;IAqEtB,OAAO,CAAC,yBAAyB;IAQjC,OAAO,CAAC,uBAAuB;IAU/B,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CA0B9B;IAED,OAAO,CAAC,YAAY;IAwBpB,OAAO,CAAC,YAAY;IASpB,OAAO,CAAC,WAAW;IAKnB,OAAO,CAAC,WAAW;CAanB","sourcesContent":["import { fuzzyFilter } from \"../fuzzy.ts\";\nimport { getKeybindings, type KeybindingsManager } from \"../keybindings.ts\";\nimport type { Component } from \"../tui.ts\";\nimport { truncateToWidth, visibleWidth, wrapTextWithAnsi } from \"../utils.ts\";\nimport { Input } from \"./input.ts\";\n\nexport interface SettingItem {\n\t/** Unique identifier for this setting */\n\tid: string;\n\t/** Display label (left side) */\n\tlabel: string;\n\t/** Optional description shown when selected */\n\tdescription?: string;\n\t/** Current value to display (right side) */\n\tcurrentValue: string;\n\t/** If provided, Enter/Space cycles through these values */\n\tvalues?: string[];\n\t/** If provided, Enter opens this submenu. Receives current value and done callback. */\n\tsubmenu?: (currentValue: string, done: (selectedValue?: string) => void) => Component;\n}\n\nexport interface SettingsListTheme {\n\tlabel: (text: string, selected: boolean) => string;\n\tvalue: (text: string, selected: boolean) => string;\n\tdescription: (text: string) => string;\n\tcursor: string;\n\thint: (text: string) => string;\n}\n\nexport interface SettingsListOptions {\n\tenableSearch?: boolean;\n}\n\nexport class SettingsList implements Component {\n\tprivate items: SettingItem[];\n\tprivate filteredItems: SettingItem[];\n\tprivate theme: SettingsListTheme;\n\tprivate selectedIndex = 0;\n\tprivate maxVisible: number;\n\tprivate onChange: (id: string, newValue: string) => void;\n\tprivate onCancel: () => void;\n\tprivate searchInput?: Input;\n\tprivate searchEnabled: boolean;\n\n\t// Submenu state\n\tprivate submenuComponent: Component | null = null;\n\tprivate submenuItemIndex: number | null = null;\n\n\tconstructor(\n\t\titems: SettingItem[],\n\t\tmaxVisible: number,\n\t\ttheme: SettingsListTheme,\n\t\tonChange: (id: string, newValue: string) => void,\n\t\tonCancel: () => void,\n\t\toptions: SettingsListOptions = {},\n\t) {\n\t\tthis.items = items;\n\t\tthis.filteredItems = items;\n\t\tthis.maxVisible = maxVisible;\n\t\tthis.theme = theme;\n\t\tthis.onChange = onChange;\n\t\tthis.onCancel = onCancel;\n\t\tthis.searchEnabled = options.enableSearch ?? false;\n\t\tif (this.searchEnabled) {\n\t\t\tthis.searchInput = new Input();\n\t\t}\n\t}\n\n\t/** Update an item's currentValue */\n\tupdateValue(id: string, newValue: string): void {\n\t\tconst item = this.items.find((i) => i.id === id);\n\t\tif (item) {\n\t\t\titem.currentValue = newValue;\n\t\t}\n\t}\n\n\tinvalidate(): void {\n\t\tthis.submenuComponent?.invalidate?.();\n\t}\n\n\trender(width: number): string[] {\n\t\t// If submenu is active, render it instead\n\t\tif (this.submenuComponent) {\n\t\t\treturn this.submenuComponent.render(width);\n\t\t}\n\n\t\treturn this.renderMainList(width);\n\t}\n\n\tprivate renderMainList(width: number): string[] {\n\t\tconst lines: string[] = [];\n\n\t\tif (this.searchEnabled && this.searchInput) {\n\t\t\tlines.push(...this.searchInput.render(width));\n\t\t\tlines.push(\"\");\n\t\t}\n\n\t\tif (this.items.length === 0) {\n\t\t\tlines.push(this.theme.hint(\" No settings available\"));\n\t\t\tif (this.searchEnabled) {\n\t\t\t\tthis.addHintLine(lines, width);\n\t\t\t}\n\t\t\treturn lines;\n\t\t}\n\n\t\tconst displayItems = this.searchEnabled ? this.filteredItems : this.items;\n\t\tif (displayItems.length === 0) {\n\t\t\tlines.push(truncateToWidth(this.theme.hint(\" No matching settings\"), width));\n\t\t\tthis.addHintLine(lines, width);\n\t\t\treturn lines;\n\t\t}\n\n\t\t// Calculate visible range with scrolling\n\t\tconst startIndex = Math.max(\n\t\t\t0,\n\t\t\tMath.min(this.selectedIndex - Math.floor(this.maxVisible / 2), displayItems.length - this.maxVisible),\n\t\t);\n\t\tconst endIndex = Math.min(startIndex + this.maxVisible, displayItems.length);\n\n\t\t// Calculate max label width for alignment\n\t\tconst maxLabelWidth = Math.min(30, Math.max(...this.items.map((item) => visibleWidth(item.label))));\n\n\t\t// Render visible items\n\t\tfor (let i = startIndex; i < endIndex; i++) {\n\t\t\tconst item = displayItems[i];\n\t\t\tif (!item) continue;\n\n\t\t\tconst isSelected = i === this.selectedIndex;\n\t\t\tconst prefix = isSelected ? this.theme.cursor : \" \";\n\t\t\tconst prefixWidth = visibleWidth(prefix);\n\n\t\t\t// Pad label to align values\n\t\t\tconst labelPadded = item.label + \" \".repeat(Math.max(0, maxLabelWidth - visibleWidth(item.label)));\n\t\t\tconst labelText = this.theme.label(labelPadded, isSelected);\n\n\t\t\t// Calculate space for value\n\t\t\tconst separator = \" \";\n\t\t\tconst usedWidth = prefixWidth + maxLabelWidth + visibleWidth(separator);\n\t\t\tconst valueMaxWidth = width - usedWidth - 2;\n\n\t\t\tconst valueText = this.theme.value(truncateToWidth(item.currentValue, valueMaxWidth, \"\"), isSelected);\n\n\t\t\tlines.push(truncateToWidth(prefix + labelText + separator + valueText, width));\n\t\t}\n\n\t\t// Add scroll indicator if needed\n\t\tif (startIndex > 0 || endIndex < displayItems.length) {\n\t\t\tconst scrollText = ` (${this.selectedIndex + 1}/${displayItems.length})`;\n\t\t\tlines.push(this.theme.hint(truncateToWidth(scrollText, width - 2, \"\")));\n\t\t}\n\n\t\tthis.appendSelectedDescription(lines, displayItems[this.selectedIndex], width);\n\n\t\t// Add hint\n\t\tthis.addHintLine(lines, width);\n\n\t\treturn lines;\n\t}\n\tprivate appendSelectedDescription(lines: string[], selectedItem: SettingItem | undefined, width: number): void {\n\t\tif (!selectedItem?.description) return;\n\t\tlines.push(\"\");\n\t\tfor (const line of wrapTextWithAnsi(selectedItem.description, width - 4)) {\n\t\t\tlines.push(this.theme.description(` ${line}`));\n\t\t}\n\t}\n\n\tprivate handleSelectionMovement(data: string, keybindings: KeybindingsManager, itemCount: number): boolean {\n\t\tif (keybindings.matches(data, \"tui.select.up\")) {\n\t\t\tif (itemCount > 0) this.selectedIndex = this.selectedIndex === 0 ? itemCount - 1 : this.selectedIndex - 1;\n\t\t\treturn true;\n\t\t}\n\t\tif (!keybindings.matches(data, \"tui.select.down\")) return false;\n\t\tif (itemCount > 0) this.selectedIndex = this.selectedIndex === itemCount - 1 ? 0 : this.selectedIndex + 1;\n\t\treturn true;\n\t}\n\n\thandleInput(data: string): void {\n\t\t// If submenu is active, delegate all input to it\n\t\t// The submenu's onCancel (triggered by escape) will call done() which closes it\n\t\tif (this.submenuComponent) {\n\t\t\tthis.submenuComponent.handleInput?.(data);\n\t\t\treturn;\n\t\t}\n\n\t\tconst keybindings = getKeybindings();\n\t\tconst displayItems = this.searchEnabled ? this.filteredItems : this.items;\n\t\tif (this.handleSelectionMovement(data, keybindings, displayItems.length)) return;\n\t\tif (\n\t\t\tkeybindings.matches(data, \"tui.select.confirm\") ||\n\t\t\t(data === \" \" && (!this.searchEnabled || this.searchInput?.getValue().length === 0))\n\t\t) {\n\t\t\tthis.activateItem();\n\t\t\treturn;\n\t\t}\n\t\tif (keybindings.matches(data, \"tui.select.cancel\")) {\n\t\t\tthis.onCancel();\n\t\t\treturn;\n\t\t}\n\t\tif (this.searchEnabled && this.searchInput) {\n\t\t\tthis.searchInput.handleInput(data);\n\t\t\tthis.applyFilter(this.searchInput.getValue());\n\t\t}\n\t}\n\n\tprivate activateItem(): void {\n\t\tconst item = this.searchEnabled ? this.filteredItems[this.selectedIndex] : this.items[this.selectedIndex];\n\t\tif (!item) return;\n\n\t\tif (item.submenu) {\n\t\t\t// Open submenu, passing current value so it can pre-select correctly\n\t\t\tthis.submenuItemIndex = this.selectedIndex;\n\t\t\tthis.submenuComponent = item.submenu(item.currentValue, (selectedValue?: string) => {\n\t\t\t\tif (selectedValue !== undefined) {\n\t\t\t\t\titem.currentValue = selectedValue;\n\t\t\t\t\tthis.onChange(item.id, selectedValue);\n\t\t\t\t}\n\t\t\t\tthis.closeSubmenu();\n\t\t\t});\n\t\t} else if (item.values && item.values.length > 0) {\n\t\t\t// Cycle through values\n\t\t\tconst currentIndex = item.values.indexOf(item.currentValue);\n\t\t\tconst nextIndex = (currentIndex + 1) % item.values.length;\n\t\t\tconst newValue = item.values[nextIndex];\n\t\t\titem.currentValue = newValue;\n\t\t\tthis.onChange(item.id, newValue);\n\t\t}\n\t}\n\n\tprivate closeSubmenu(): void {\n\t\tthis.submenuComponent = null;\n\t\t// Restore selection to the item that opened the submenu\n\t\tif (this.submenuItemIndex !== null) {\n\t\t\tthis.selectedIndex = this.submenuItemIndex;\n\t\t\tthis.submenuItemIndex = null;\n\t\t}\n\t}\n\n\tprivate applyFilter(query: string): void {\n\t\tthis.filteredItems = fuzzyFilter(this.items, query, (item) => item.label);\n\t\tthis.selectedIndex = 0;\n\t}\n\n\tprivate addHintLine(lines: string[], width: number): void {\n\t\tlines.push(\"\");\n\t\tlines.push(\n\t\t\ttruncateToWidth(\n\t\t\t\tthis.theme.hint(\n\t\t\t\t\tthis.searchEnabled\n\t\t\t\t\t\t? \" Type to search · Enter/Space to change · Esc to cancel\"\n\t\t\t\t\t\t: \" Enter/Space to change · Esc to cancel\",\n\t\t\t\t),\n\t\t\t\twidth,\n\t\t\t),\n\t\t);\n\t}\n}\n"]}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { fuzzyFilter } from "../fuzzy.js";
|
|
2
|
+
import { getKeybindings } from "../keybindings.js";
|
|
3
|
+
import { truncateToWidth, visibleWidth, wrapTextWithAnsi } from "../utils.js";
|
|
4
|
+
import { Input } from "./input.js";
|
|
5
|
+
export class SettingsList {
|
|
6
|
+
items;
|
|
7
|
+
filteredItems;
|
|
8
|
+
theme;
|
|
9
|
+
selectedIndex = 0;
|
|
10
|
+
maxVisible;
|
|
11
|
+
onChange;
|
|
12
|
+
onCancel;
|
|
13
|
+
searchInput;
|
|
14
|
+
searchEnabled;
|
|
15
|
+
// Submenu state
|
|
16
|
+
submenuComponent = null;
|
|
17
|
+
submenuItemIndex = null;
|
|
18
|
+
constructor(items, maxVisible, theme, onChange, onCancel, options = {}) {
|
|
19
|
+
this.items = items;
|
|
20
|
+
this.filteredItems = items;
|
|
21
|
+
this.maxVisible = maxVisible;
|
|
22
|
+
this.theme = theme;
|
|
23
|
+
this.onChange = onChange;
|
|
24
|
+
this.onCancel = onCancel;
|
|
25
|
+
this.searchEnabled = options.enableSearch ?? false;
|
|
26
|
+
if (this.searchEnabled) {
|
|
27
|
+
this.searchInput = new Input();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/** Update an item's currentValue */
|
|
31
|
+
updateValue(id, newValue) {
|
|
32
|
+
const item = this.items.find((i) => i.id === id);
|
|
33
|
+
if (item) {
|
|
34
|
+
item.currentValue = newValue;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
invalidate() {
|
|
38
|
+
this.submenuComponent?.invalidate?.();
|
|
39
|
+
}
|
|
40
|
+
render(width) {
|
|
41
|
+
// If submenu is active, render it instead
|
|
42
|
+
if (this.submenuComponent) {
|
|
43
|
+
return this.submenuComponent.render(width);
|
|
44
|
+
}
|
|
45
|
+
return this.renderMainList(width);
|
|
46
|
+
}
|
|
47
|
+
renderMainList(width) {
|
|
48
|
+
const lines = [];
|
|
49
|
+
if (this.searchEnabled && this.searchInput) {
|
|
50
|
+
lines.push(...this.searchInput.render(width));
|
|
51
|
+
lines.push("");
|
|
52
|
+
}
|
|
53
|
+
if (this.items.length === 0) {
|
|
54
|
+
lines.push(this.theme.hint(" No settings available"));
|
|
55
|
+
if (this.searchEnabled) {
|
|
56
|
+
this.addHintLine(lines, width);
|
|
57
|
+
}
|
|
58
|
+
return lines;
|
|
59
|
+
}
|
|
60
|
+
const displayItems = this.searchEnabled ? this.filteredItems : this.items;
|
|
61
|
+
if (displayItems.length === 0) {
|
|
62
|
+
lines.push(truncateToWidth(this.theme.hint(" No matching settings"), width));
|
|
63
|
+
this.addHintLine(lines, width);
|
|
64
|
+
return lines;
|
|
65
|
+
}
|
|
66
|
+
// Calculate visible range with scrolling
|
|
67
|
+
const startIndex = Math.max(0, Math.min(this.selectedIndex - Math.floor(this.maxVisible / 2), displayItems.length - this.maxVisible));
|
|
68
|
+
const endIndex = Math.min(startIndex + this.maxVisible, displayItems.length);
|
|
69
|
+
// Calculate max label width for alignment
|
|
70
|
+
const maxLabelWidth = Math.min(30, Math.max(...this.items.map((item) => visibleWidth(item.label))));
|
|
71
|
+
// Render visible items
|
|
72
|
+
for (let i = startIndex; i < endIndex; i++) {
|
|
73
|
+
const item = displayItems[i];
|
|
74
|
+
if (!item)
|
|
75
|
+
continue;
|
|
76
|
+
const isSelected = i === this.selectedIndex;
|
|
77
|
+
const prefix = isSelected ? this.theme.cursor : " ";
|
|
78
|
+
const prefixWidth = visibleWidth(prefix);
|
|
79
|
+
// Pad label to align values
|
|
80
|
+
const labelPadded = item.label + " ".repeat(Math.max(0, maxLabelWidth - visibleWidth(item.label)));
|
|
81
|
+
const labelText = this.theme.label(labelPadded, isSelected);
|
|
82
|
+
// Calculate space for value
|
|
83
|
+
const separator = " ";
|
|
84
|
+
const usedWidth = prefixWidth + maxLabelWidth + visibleWidth(separator);
|
|
85
|
+
const valueMaxWidth = width - usedWidth - 2;
|
|
86
|
+
const valueText = this.theme.value(truncateToWidth(item.currentValue, valueMaxWidth, ""), isSelected);
|
|
87
|
+
lines.push(truncateToWidth(prefix + labelText + separator + valueText, width));
|
|
88
|
+
}
|
|
89
|
+
// Add scroll indicator if needed
|
|
90
|
+
if (startIndex > 0 || endIndex < displayItems.length) {
|
|
91
|
+
const scrollText = ` (${this.selectedIndex + 1}/${displayItems.length})`;
|
|
92
|
+
lines.push(this.theme.hint(truncateToWidth(scrollText, width - 2, "")));
|
|
93
|
+
}
|
|
94
|
+
this.appendSelectedDescription(lines, displayItems[this.selectedIndex], width);
|
|
95
|
+
// Add hint
|
|
96
|
+
this.addHintLine(lines, width);
|
|
97
|
+
return lines;
|
|
98
|
+
}
|
|
99
|
+
appendSelectedDescription(lines, selectedItem, width) {
|
|
100
|
+
if (!selectedItem?.description)
|
|
101
|
+
return;
|
|
102
|
+
lines.push("");
|
|
103
|
+
for (const line of wrapTextWithAnsi(selectedItem.description, width - 4)) {
|
|
104
|
+
lines.push(this.theme.description(` ${line}`));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
handleSelectionMovement(data, keybindings, itemCount) {
|
|
108
|
+
if (keybindings.matches(data, "tui.select.up")) {
|
|
109
|
+
if (itemCount > 0)
|
|
110
|
+
this.selectedIndex = this.selectedIndex === 0 ? itemCount - 1 : this.selectedIndex - 1;
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
if (!keybindings.matches(data, "tui.select.down"))
|
|
114
|
+
return false;
|
|
115
|
+
if (itemCount > 0)
|
|
116
|
+
this.selectedIndex = this.selectedIndex === itemCount - 1 ? 0 : this.selectedIndex + 1;
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
handleInput(data) {
|
|
120
|
+
// If submenu is active, delegate all input to it
|
|
121
|
+
// The submenu's onCancel (triggered by escape) will call done() which closes it
|
|
122
|
+
if (this.submenuComponent) {
|
|
123
|
+
this.submenuComponent.handleInput?.(data);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const keybindings = getKeybindings();
|
|
127
|
+
const displayItems = this.searchEnabled ? this.filteredItems : this.items;
|
|
128
|
+
if (this.handleSelectionMovement(data, keybindings, displayItems.length))
|
|
129
|
+
return;
|
|
130
|
+
if (keybindings.matches(data, "tui.select.confirm") ||
|
|
131
|
+
(data === " " && (!this.searchEnabled || this.searchInput?.getValue().length === 0))) {
|
|
132
|
+
this.activateItem();
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (keybindings.matches(data, "tui.select.cancel")) {
|
|
136
|
+
this.onCancel();
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (this.searchEnabled && this.searchInput) {
|
|
140
|
+
this.searchInput.handleInput(data);
|
|
141
|
+
this.applyFilter(this.searchInput.getValue());
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
activateItem() {
|
|
145
|
+
const item = this.searchEnabled ? this.filteredItems[this.selectedIndex] : this.items[this.selectedIndex];
|
|
146
|
+
if (!item)
|
|
147
|
+
return;
|
|
148
|
+
if (item.submenu) {
|
|
149
|
+
// Open submenu, passing current value so it can pre-select correctly
|
|
150
|
+
this.submenuItemIndex = this.selectedIndex;
|
|
151
|
+
this.submenuComponent = item.submenu(item.currentValue, (selectedValue) => {
|
|
152
|
+
if (selectedValue !== undefined) {
|
|
153
|
+
item.currentValue = selectedValue;
|
|
154
|
+
this.onChange(item.id, selectedValue);
|
|
155
|
+
}
|
|
156
|
+
this.closeSubmenu();
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
else if (item.values && item.values.length > 0) {
|
|
160
|
+
// Cycle through values
|
|
161
|
+
const currentIndex = item.values.indexOf(item.currentValue);
|
|
162
|
+
const nextIndex = (currentIndex + 1) % item.values.length;
|
|
163
|
+
const newValue = item.values[nextIndex];
|
|
164
|
+
item.currentValue = newValue;
|
|
165
|
+
this.onChange(item.id, newValue);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
closeSubmenu() {
|
|
169
|
+
this.submenuComponent = null;
|
|
170
|
+
// Restore selection to the item that opened the submenu
|
|
171
|
+
if (this.submenuItemIndex !== null) {
|
|
172
|
+
this.selectedIndex = this.submenuItemIndex;
|
|
173
|
+
this.submenuItemIndex = null;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
applyFilter(query) {
|
|
177
|
+
this.filteredItems = fuzzyFilter(this.items, query, (item) => item.label);
|
|
178
|
+
this.selectedIndex = 0;
|
|
179
|
+
}
|
|
180
|
+
addHintLine(lines, width) {
|
|
181
|
+
lines.push("");
|
|
182
|
+
lines.push(truncateToWidth(this.theme.hint(this.searchEnabled
|
|
183
|
+
? " Type to search · Enter/Space to change · Esc to cancel"
|
|
184
|
+
: " Enter/Space to change · Esc to cancel"), width));
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
//# sourceMappingURL=settings-list.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settings-list.js","sourceRoot":"","sources":["../../src/components/settings-list.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,cAAc,EAA2B,MAAM,mBAAmB,CAAC;AAE5E,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC9E,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AA6BnC,MAAM,OAAO,YAAY;IAChB,KAAK,CAAgB;IACrB,aAAa,CAAgB;IAC7B,KAAK,CAAoB;IACzB,aAAa,GAAG,CAAC,CAAC;IAClB,UAAU,CAAS;IACnB,QAAQ,CAAyC;IACjD,QAAQ,CAAa;IACrB,WAAW,CAAS;IACpB,aAAa,CAAU;IAE/B,gBAAgB;IACR,gBAAgB,GAAqB,IAAI,CAAC;IAC1C,gBAAgB,GAAkB,IAAI,CAAC;IAE/C,YACC,KAAoB,EACpB,UAAkB,EAClB,KAAwB,EACxB,QAAgD,EAChD,QAAoB,EACpB,OAAO,GAAwB,EAAE,EAChC;QACD,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,aAAa,GAAG,KAAK,CAAC;QAC3B,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAC7B,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,aAAa,GAAG,OAAO,CAAC,YAAY,IAAI,KAAK,CAAC;QACnD,IAAI,IAAI,CAAC,aAAa,EAAE,CAAC;YACxB,IAAI,CAAC,WAAW,GAAG,IAAI,KAAK,EAAE,CAAC;QAChC,CAAC;IAAA,CACD;IAED,oCAAoC;IACpC,WAAW,CAAC,EAAU,EAAE,QAAgB,EAAQ;QAC/C,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;QACjD,IAAI,IAAI,EAAE,CAAC;YACV,IAAI,CAAC,YAAY,GAAG,QAAQ,CAAC;QAC9B,CAAC;IAAA,CACD;IAED,UAAU,GAAS;QAClB,IAAI,CAAC,gBAAgB,EAAE,UAAU,EAAE,EAAE,CAAC;IAAA,CACtC;IAED,MAAM,CAAC,KAAa,EAAY;QAC/B,0CAA0C;QAC1C,IAAI,IAAI,CAAC,gBAAgB,EAAE,CAAC;YAC3B,OAAO,IAAI,CAAC,gBAAgB,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAC5C,CAAC;QAED,OAAO,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC;IAAA,CAClC;IAEO,cAAc,CAAC,KAAa,EAAY;QAC/C,MAAM,KAAK,GAAa,EAAE,CAAC;QAE3B,IAAI,IAAI,CAAC,aAAa,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;YAC5C,KAAK,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;YAC9C,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAChB,CAAC;QAED,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC7B,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,yBAAyB,CAAC,CAAC,CAAC;YACvD,IAAI,IAAI,CAAC,aAAa,EAAE,CAAC;gBACxB,IAAI,CAAC,WAAW,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;YAChC,CAAC;YACD,OAAO,KAAK,CAAC;QACd,CAAC;QAED,MAAM,YAAY,GAAG,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC;QAC1E,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC/B,KAAK,CAAC,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,wBAAwB,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC;YAC9E,IAAI,CAAC,WAAW,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;YAC/B,OAAO,KAAK,CAAC;QACd,CAAC;QAED,yCAAyC;QACzC,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,CAC1B,CAAC,EACD,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,UAAU,GAAG,CAAC,CAAC,EAAE,YAAY,CAAC,MAAM,GAAG,IAAI,CAAC,UAAU,CAAC,CACrG,CAAC;QACF,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,UAAU,GAAG,IAAI,CAAC,UAAU,EAAE,YAAY,CAAC,MAAM,CAAC,CAAC;QAE7E,0CAA0C;QAC1C,MAAM,aAAa,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;QAEpG,uBAAuB;QACvB,KAAK,IAAI,CAAC,GAAG,UAAU,EAAE,CAAC,GAAG,QAAQ,EAAE,CAAC,EAAE,EAAE,CAAC;YAC5C,MAAM,IAAI,GAAG,YAAY,CAAC,CAAC,CAAC,CAAC;YAC7B,IAAI,CAAC,IAAI;gBAAE,SAAS;YAEpB,MAAM,UAAU,GAAG,CAAC,KAAK,IAAI,CAAC,aAAa,CAAC;YAC5C,MAAM,MAAM,GAAG,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC;YACrD,MAAM,WAAW,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC;YAEzC,4BAA4B;YAC5B,MAAM,WAAW,GAAG,IAAI,CAAC,KAAK,GAAG,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,aAAa,GAAG,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;YACnG,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,WAAW,EAAE,UAAU,CAAC,CAAC;YAE5D,4BAA4B;YAC5B,MAAM,SAAS,GAAG,IAAI,CAAC;YACvB,MAAM,SAAS,GAAG,WAAW,GAAG,aAAa,GAAG,YAAY,CAAC,SAAS,CAAC,CAAC;YACxE,MAAM,aAAa,GAAG,KAAK,GAAG,SAAS,GAAG,CAAC,CAAC;YAE5C,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,eAAe,CAAC,IAAI,CAAC,YAAY,EAAE,aAAa,EAAE,EAAE,CAAC,EAAE,UAAU,CAAC,CAAC;YAEtG,KAAK,CAAC,IAAI,CAAC,eAAe,CAAC,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,EAAE,KAAK,CAAC,CAAC,CAAC;QAChF,CAAC;QAED,iCAAiC;QACjC,IAAI,UAAU,GAAG,CAAC,IAAI,QAAQ,GAAG,YAAY,CAAC,MAAM,EAAE,CAAC;YACtD,MAAM,UAAU,GAAG,MAAM,IAAI,CAAC,aAAa,GAAG,CAAC,IAAI,YAAY,CAAC,MAAM,GAAG,CAAC;YAC1E,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,eAAe,CAAC,UAAU,EAAE,KAAK,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC;QACzE,CAAC;QAED,IAAI,CAAC,yBAAyB,CAAC,KAAK,EAAE,YAAY,CAAC,IAAI,CAAC,aAAa,CAAC,EAAE,KAAK,CAAC,CAAC;QAE/E,WAAW;QACX,IAAI,CAAC,WAAW,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;QAE/B,OAAO,KAAK,CAAC;IAAA,CACb;IACO,yBAAyB,CAAC,KAAe,EAAE,YAAqC,EAAE,KAAa,EAAQ;QAC9G,IAAI,CAAC,YAAY,EAAE,WAAW;YAAE,OAAO;QACvC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACf,KAAK,MAAM,IAAI,IAAI,gBAAgB,CAAC,YAAY,CAAC,WAAW,EAAE,KAAK,GAAG,CAAC,CAAC,EAAE,CAAC;YAC1E,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,CAAC;QACjD,CAAC;IAAA,CACD;IAEO,uBAAuB,CAAC,IAAY,EAAE,WAA+B,EAAE,SAAiB,EAAW;QAC1G,IAAI,WAAW,CAAC,OAAO,CAAC,IAAI,EAAE,eAAe,CAAC,EAAE,CAAC;YAChD,IAAI,SAAS,GAAG,CAAC;gBAAE,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC,aAAa,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,aAAa,GAAG,CAAC,CAAC;YAC1G,OAAO,IAAI,CAAC;QACb,CAAC;QACD,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,IAAI,EAAE,iBAAiB,CAAC;YAAE,OAAO,KAAK,CAAC;QAChE,IAAI,SAAS,GAAG,CAAC;YAAE,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC,aAAa,KAAK,SAAS,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,aAAa,GAAG,CAAC,CAAC;QAC1G,OAAO,IAAI,CAAC;IAAA,CACZ;IAED,WAAW,CAAC,IAAY,EAAQ;QAC/B,iDAAiD;QACjD,gFAAgF;QAChF,IAAI,IAAI,CAAC,gBAAgB,EAAE,CAAC;YAC3B,IAAI,CAAC,gBAAgB,CAAC,WAAW,EAAE,CAAC,IAAI,CAAC,CAAC;YAC1C,OAAO;QACR,CAAC;QAED,MAAM,WAAW,GAAG,cAAc,EAAE,CAAC;QACrC,MAAM,YAAY,GAAG,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC;QAC1E,IAAI,IAAI,CAAC,uBAAuB,CAAC,IAAI,EAAE,WAAW,EAAE,YAAY,CAAC,MAAM,CAAC;YAAE,OAAO;QACjF,IACC,WAAW,CAAC,OAAO,CAAC,IAAI,EAAE,oBAAoB,CAAC;YAC/C,CAAC,IAAI,KAAK,GAAG,IAAI,CAAC,CAAC,IAAI,CAAC,aAAa,IAAI,IAAI,CAAC,WAAW,EAAE,QAAQ,EAAE,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,EACnF,CAAC;YACF,IAAI,CAAC,YAAY,EAAE,CAAC;YACpB,OAAO;QACR,CAAC;QACD,IAAI,WAAW,CAAC,OAAO,CAAC,IAAI,EAAE,mBAAmB,CAAC,EAAE,CAAC;YACpD,IAAI,CAAC,QAAQ,EAAE,CAAC;YAChB,OAAO;QACR,CAAC;QACD,IAAI,IAAI,CAAC,aAAa,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;YAC5C,IAAI,CAAC,WAAW,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;YACnC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,WAAW,CAAC,QAAQ,EAAE,CAAC,CAAC;QAC/C,CAAC;IAAA,CACD;IAEO,YAAY,GAAS;QAC5B,MAAM,IAAI,GAAG,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;QAC1G,IAAI,CAAC,IAAI;YAAE,OAAO;QAElB,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YAClB,qEAAqE;YACrE,IAAI,CAAC,gBAAgB,GAAG,IAAI,CAAC,aAAa,CAAC;YAC3C,IAAI,CAAC,gBAAgB,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC,aAAsB,EAAE,EAAE,CAAC;gBACnF,IAAI,aAAa,KAAK,SAAS,EAAE,CAAC;oBACjC,IAAI,CAAC,YAAY,GAAG,aAAa,CAAC;oBAClC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,EAAE,aAAa,CAAC,CAAC;gBACvC,CAAC;gBACD,IAAI,CAAC,YAAY,EAAE,CAAC;YAAA,CACpB,CAAC,CAAC;QACJ,CAAC;aAAM,IAAI,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAClD,uBAAuB;YACvB,MAAM,YAAY,GAAG,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;YAC5D,MAAM,SAAS,GAAG,CAAC,YAAY,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC;YAC1D,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;YACxC,IAAI,CAAC,YAAY,GAAG,QAAQ,CAAC;YAC7B,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,EAAE,QAAQ,CAAC,CAAC;QAClC,CAAC;IAAA,CACD;IAEO,YAAY,GAAS;QAC5B,IAAI,CAAC,gBAAgB,GAAG,IAAI,CAAC;QAC7B,wDAAwD;QACxD,IAAI,IAAI,CAAC,gBAAgB,KAAK,IAAI,EAAE,CAAC;YACpC,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC,gBAAgB,CAAC;YAC3C,IAAI,CAAC,gBAAgB,GAAG,IAAI,CAAC;QAC9B,CAAC;IAAA,CACD;IAEO,WAAW,CAAC,KAAa,EAAQ;QACxC,IAAI,CAAC,aAAa,GAAG,WAAW,CAAC,IAAI,CAAC,KAAK,EAAE,KAAK,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC1E,IAAI,CAAC,aAAa,GAAG,CAAC,CAAC;IAAA,CACvB;IAEO,WAAW,CAAC,KAAe,EAAE,KAAa,EAAQ;QACzD,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACf,KAAK,CAAC,IAAI,CACT,eAAe,CACd,IAAI,CAAC,KAAK,CAAC,IAAI,CACd,IAAI,CAAC,aAAa;YACjB,CAAC,CAAC,4DAA0D;YAC5D,CAAC,CAAC,0CAAyC,CAC5C,EACD,KAAK,CACL,CACD,CAAC;IAAA,CACF;CACD","sourcesContent":["import { fuzzyFilter } from \"../fuzzy.ts\";\nimport { getKeybindings, type KeybindingsManager } from \"../keybindings.ts\";\nimport type { Component } from \"../tui.ts\";\nimport { truncateToWidth, visibleWidth, wrapTextWithAnsi } from \"../utils.ts\";\nimport { Input } from \"./input.ts\";\n\nexport interface SettingItem {\n\t/** Unique identifier for this setting */\n\tid: string;\n\t/** Display label (left side) */\n\tlabel: string;\n\t/** Optional description shown when selected */\n\tdescription?: string;\n\t/** Current value to display (right side) */\n\tcurrentValue: string;\n\t/** If provided, Enter/Space cycles through these values */\n\tvalues?: string[];\n\t/** If provided, Enter opens this submenu. Receives current value and done callback. */\n\tsubmenu?: (currentValue: string, done: (selectedValue?: string) => void) => Component;\n}\n\nexport interface SettingsListTheme {\n\tlabel: (text: string, selected: boolean) => string;\n\tvalue: (text: string, selected: boolean) => string;\n\tdescription: (text: string) => string;\n\tcursor: string;\n\thint: (text: string) => string;\n}\n\nexport interface SettingsListOptions {\n\tenableSearch?: boolean;\n}\n\nexport class SettingsList implements Component {\n\tprivate items: SettingItem[];\n\tprivate filteredItems: SettingItem[];\n\tprivate theme: SettingsListTheme;\n\tprivate selectedIndex = 0;\n\tprivate maxVisible: number;\n\tprivate onChange: (id: string, newValue: string) => void;\n\tprivate onCancel: () => void;\n\tprivate searchInput?: Input;\n\tprivate searchEnabled: boolean;\n\n\t// Submenu state\n\tprivate submenuComponent: Component | null = null;\n\tprivate submenuItemIndex: number | null = null;\n\n\tconstructor(\n\t\titems: SettingItem[],\n\t\tmaxVisible: number,\n\t\ttheme: SettingsListTheme,\n\t\tonChange: (id: string, newValue: string) => void,\n\t\tonCancel: () => void,\n\t\toptions: SettingsListOptions = {},\n\t) {\n\t\tthis.items = items;\n\t\tthis.filteredItems = items;\n\t\tthis.maxVisible = maxVisible;\n\t\tthis.theme = theme;\n\t\tthis.onChange = onChange;\n\t\tthis.onCancel = onCancel;\n\t\tthis.searchEnabled = options.enableSearch ?? false;\n\t\tif (this.searchEnabled) {\n\t\t\tthis.searchInput = new Input();\n\t\t}\n\t}\n\n\t/** Update an item's currentValue */\n\tupdateValue(id: string, newValue: string): void {\n\t\tconst item = this.items.find((i) => i.id === id);\n\t\tif (item) {\n\t\t\titem.currentValue = newValue;\n\t\t}\n\t}\n\n\tinvalidate(): void {\n\t\tthis.submenuComponent?.invalidate?.();\n\t}\n\n\trender(width: number): string[] {\n\t\t// If submenu is active, render it instead\n\t\tif (this.submenuComponent) {\n\t\t\treturn this.submenuComponent.render(width);\n\t\t}\n\n\t\treturn this.renderMainList(width);\n\t}\n\n\tprivate renderMainList(width: number): string[] {\n\t\tconst lines: string[] = [];\n\n\t\tif (this.searchEnabled && this.searchInput) {\n\t\t\tlines.push(...this.searchInput.render(width));\n\t\t\tlines.push(\"\");\n\t\t}\n\n\t\tif (this.items.length === 0) {\n\t\t\tlines.push(this.theme.hint(\" No settings available\"));\n\t\t\tif (this.searchEnabled) {\n\t\t\t\tthis.addHintLine(lines, width);\n\t\t\t}\n\t\t\treturn lines;\n\t\t}\n\n\t\tconst displayItems = this.searchEnabled ? this.filteredItems : this.items;\n\t\tif (displayItems.length === 0) {\n\t\t\tlines.push(truncateToWidth(this.theme.hint(\" No matching settings\"), width));\n\t\t\tthis.addHintLine(lines, width);\n\t\t\treturn lines;\n\t\t}\n\n\t\t// Calculate visible range with scrolling\n\t\tconst startIndex = Math.max(\n\t\t\t0,\n\t\t\tMath.min(this.selectedIndex - Math.floor(this.maxVisible / 2), displayItems.length - this.maxVisible),\n\t\t);\n\t\tconst endIndex = Math.min(startIndex + this.maxVisible, displayItems.length);\n\n\t\t// Calculate max label width for alignment\n\t\tconst maxLabelWidth = Math.min(30, Math.max(...this.items.map((item) => visibleWidth(item.label))));\n\n\t\t// Render visible items\n\t\tfor (let i = startIndex; i < endIndex; i++) {\n\t\t\tconst item = displayItems[i];\n\t\t\tif (!item) continue;\n\n\t\t\tconst isSelected = i === this.selectedIndex;\n\t\t\tconst prefix = isSelected ? this.theme.cursor : \" \";\n\t\t\tconst prefixWidth = visibleWidth(prefix);\n\n\t\t\t// Pad label to align values\n\t\t\tconst labelPadded = item.label + \" \".repeat(Math.max(0, maxLabelWidth - visibleWidth(item.label)));\n\t\t\tconst labelText = this.theme.label(labelPadded, isSelected);\n\n\t\t\t// Calculate space for value\n\t\t\tconst separator = \" \";\n\t\t\tconst usedWidth = prefixWidth + maxLabelWidth + visibleWidth(separator);\n\t\t\tconst valueMaxWidth = width - usedWidth - 2;\n\n\t\t\tconst valueText = this.theme.value(truncateToWidth(item.currentValue, valueMaxWidth, \"\"), isSelected);\n\n\t\t\tlines.push(truncateToWidth(prefix + labelText + separator + valueText, width));\n\t\t}\n\n\t\t// Add scroll indicator if needed\n\t\tif (startIndex > 0 || endIndex < displayItems.length) {\n\t\t\tconst scrollText = ` (${this.selectedIndex + 1}/${displayItems.length})`;\n\t\t\tlines.push(this.theme.hint(truncateToWidth(scrollText, width - 2, \"\")));\n\t\t}\n\n\t\tthis.appendSelectedDescription(lines, displayItems[this.selectedIndex], width);\n\n\t\t// Add hint\n\t\tthis.addHintLine(lines, width);\n\n\t\treturn lines;\n\t}\n\tprivate appendSelectedDescription(lines: string[], selectedItem: SettingItem | undefined, width: number): void {\n\t\tif (!selectedItem?.description) return;\n\t\tlines.push(\"\");\n\t\tfor (const line of wrapTextWithAnsi(selectedItem.description, width - 4)) {\n\t\t\tlines.push(this.theme.description(` ${line}`));\n\t\t}\n\t}\n\n\tprivate handleSelectionMovement(data: string, keybindings: KeybindingsManager, itemCount: number): boolean {\n\t\tif (keybindings.matches(data, \"tui.select.up\")) {\n\t\t\tif (itemCount > 0) this.selectedIndex = this.selectedIndex === 0 ? itemCount - 1 : this.selectedIndex - 1;\n\t\t\treturn true;\n\t\t}\n\t\tif (!keybindings.matches(data, \"tui.select.down\")) return false;\n\t\tif (itemCount > 0) this.selectedIndex = this.selectedIndex === itemCount - 1 ? 0 : this.selectedIndex + 1;\n\t\treturn true;\n\t}\n\n\thandleInput(data: string): void {\n\t\t// If submenu is active, delegate all input to it\n\t\t// The submenu's onCancel (triggered by escape) will call done() which closes it\n\t\tif (this.submenuComponent) {\n\t\t\tthis.submenuComponent.handleInput?.(data);\n\t\t\treturn;\n\t\t}\n\n\t\tconst keybindings = getKeybindings();\n\t\tconst displayItems = this.searchEnabled ? this.filteredItems : this.items;\n\t\tif (this.handleSelectionMovement(data, keybindings, displayItems.length)) return;\n\t\tif (\n\t\t\tkeybindings.matches(data, \"tui.select.confirm\") ||\n\t\t\t(data === \" \" && (!this.searchEnabled || this.searchInput?.getValue().length === 0))\n\t\t) {\n\t\t\tthis.activateItem();\n\t\t\treturn;\n\t\t}\n\t\tif (keybindings.matches(data, \"tui.select.cancel\")) {\n\t\t\tthis.onCancel();\n\t\t\treturn;\n\t\t}\n\t\tif (this.searchEnabled && this.searchInput) {\n\t\t\tthis.searchInput.handleInput(data);\n\t\t\tthis.applyFilter(this.searchInput.getValue());\n\t\t}\n\t}\n\n\tprivate activateItem(): void {\n\t\tconst item = this.searchEnabled ? this.filteredItems[this.selectedIndex] : this.items[this.selectedIndex];\n\t\tif (!item) return;\n\n\t\tif (item.submenu) {\n\t\t\t// Open submenu, passing current value so it can pre-select correctly\n\t\t\tthis.submenuItemIndex = this.selectedIndex;\n\t\t\tthis.submenuComponent = item.submenu(item.currentValue, (selectedValue?: string) => {\n\t\t\t\tif (selectedValue !== undefined) {\n\t\t\t\t\titem.currentValue = selectedValue;\n\t\t\t\t\tthis.onChange(item.id, selectedValue);\n\t\t\t\t}\n\t\t\t\tthis.closeSubmenu();\n\t\t\t});\n\t\t} else if (item.values && item.values.length > 0) {\n\t\t\t// Cycle through values\n\t\t\tconst currentIndex = item.values.indexOf(item.currentValue);\n\t\t\tconst nextIndex = (currentIndex + 1) % item.values.length;\n\t\t\tconst newValue = item.values[nextIndex];\n\t\t\titem.currentValue = newValue;\n\t\t\tthis.onChange(item.id, newValue);\n\t\t}\n\t}\n\n\tprivate closeSubmenu(): void {\n\t\tthis.submenuComponent = null;\n\t\t// Restore selection to the item that opened the submenu\n\t\tif (this.submenuItemIndex !== null) {\n\t\t\tthis.selectedIndex = this.submenuItemIndex;\n\t\t\tthis.submenuItemIndex = null;\n\t\t}\n\t}\n\n\tprivate applyFilter(query: string): void {\n\t\tthis.filteredItems = fuzzyFilter(this.items, query, (item) => item.label);\n\t\tthis.selectedIndex = 0;\n\t}\n\n\tprivate addHintLine(lines: string[], width: number): void {\n\t\tlines.push(\"\");\n\t\tlines.push(\n\t\t\ttruncateToWidth(\n\t\t\t\tthis.theme.hint(\n\t\t\t\t\tthis.searchEnabled\n\t\t\t\t\t\t? \" Type to search · Enter/Space to change · Esc to cancel\"\n\t\t\t\t\t\t: \" Enter/Space to change · Esc to cancel\",\n\t\t\t\t),\n\t\t\t\twidth,\n\t\t\t),\n\t\t);\n\t}\n}\n"]}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Component } from "../tui.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Spacer component that renders empty lines
|
|
4
|
+
*/
|
|
5
|
+
export declare class Spacer implements Component {
|
|
6
|
+
private lines;
|
|
7
|
+
constructor(lines?: number);
|
|
8
|
+
setLines(lines: number): void;
|
|
9
|
+
invalidate(): void;
|
|
10
|
+
render(_width: number): string[];
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=spacer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spacer.d.ts","sourceRoot":"","sources":["../../src/components/spacer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAE3C;;GAEG;AACH,qBAAa,MAAO,YAAW,SAAS;IACvC,OAAO,CAAC,KAAK,CAAS;IAEtB,YAAY,KAAK,GAAE,MAAU,EAE5B;IAED,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAE5B;IAED,UAAU,IAAI,IAAI,CAEjB;IAED,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAM/B;CACD","sourcesContent":["import type { Component } from \"../tui.ts\";\n\n/**\n * Spacer component that renders empty lines\n */\nexport class Spacer implements Component {\n\tprivate lines: number;\n\n\tconstructor(lines: number = 1) {\n\t\tthis.lines = lines;\n\t}\n\n\tsetLines(lines: number): void {\n\t\tthis.lines = lines;\n\t}\n\n\tinvalidate(): void {\n\t\t// No cached state to invalidate currently\n\t}\n\n\trender(_width: number): string[] {\n\t\tconst result: string[] = [];\n\t\tfor (let i = 0; i < this.lines; i++) {\n\t\t\tresult.push(\"\");\n\t\t}\n\t\treturn result;\n\t}\n}\n"]}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spacer component that renders empty lines
|
|
3
|
+
*/
|
|
4
|
+
export class Spacer {
|
|
5
|
+
lines;
|
|
6
|
+
constructor(lines = 1) {
|
|
7
|
+
this.lines = lines;
|
|
8
|
+
}
|
|
9
|
+
setLines(lines) {
|
|
10
|
+
this.lines = lines;
|
|
11
|
+
}
|
|
12
|
+
invalidate() {
|
|
13
|
+
// No cached state to invalidate currently
|
|
14
|
+
}
|
|
15
|
+
render(_width) {
|
|
16
|
+
const result = [];
|
|
17
|
+
for (let i = 0; i < this.lines; i++) {
|
|
18
|
+
result.push("");
|
|
19
|
+
}
|
|
20
|
+
return result;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=spacer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spacer.js","sourceRoot":"","sources":["../../src/components/spacer.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,MAAM,OAAO,MAAM;IACV,KAAK,CAAS;IAEtB,YAAY,KAAK,GAAW,CAAC,EAAE;QAC9B,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;IAAA,CACnB;IAED,QAAQ,CAAC,KAAa,EAAQ;QAC7B,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;IAAA,CACnB;IAED,UAAU,GAAS;QAClB,0CAA0C;IADvB,CAEnB;IAED,MAAM,CAAC,MAAc,EAAY;QAChC,MAAM,MAAM,GAAa,EAAE,CAAC;QAC5B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC;YACrC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACjB,CAAC;QACD,OAAO,MAAM,CAAC;IAAA,CACd;CACD","sourcesContent":["import type { Component } from \"../tui.ts\";\n\n/**\n * Spacer component that renders empty lines\n */\nexport class Spacer implements Component {\n\tprivate lines: number;\n\n\tconstructor(lines: number = 1) {\n\t\tthis.lines = lines;\n\t}\n\n\tsetLines(lines: number): void {\n\t\tthis.lines = lines;\n\t}\n\n\tinvalidate(): void {\n\t\t// No cached state to invalidate currently\n\t}\n\n\trender(_width: number): string[] {\n\t\tconst result: string[] = [];\n\t\tfor (let i = 0; i < this.lines; i++) {\n\t\t\tresult.push(\"\");\n\t\t}\n\t\treturn result;\n\t}\n}\n"]}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { LAYOUT_NODE, type LayoutViewport, type StackAlignment, type StackLayoutEntry, type StackLayoutNode, type StackLayoutType } from "../layout-node.ts";
|
|
2
|
+
import { type Component, Container } from "../tui.ts";
|
|
3
|
+
export interface StackEntryOptions {
|
|
4
|
+
basis?: number | "auto";
|
|
5
|
+
grow?: number;
|
|
6
|
+
shrink?: number;
|
|
7
|
+
minSize?: number;
|
|
8
|
+
maxSize?: number;
|
|
9
|
+
visible?: (viewport: LayoutViewport) => boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface StackEntry extends StackEntryOptions {
|
|
12
|
+
component: Component;
|
|
13
|
+
}
|
|
14
|
+
export type StackChild = Component | StackEntry;
|
|
15
|
+
export interface StackOptions {
|
|
16
|
+
gap?: number;
|
|
17
|
+
align?: StackAlignment;
|
|
18
|
+
}
|
|
19
|
+
export declare abstract class Stack extends Container {
|
|
20
|
+
protected readonly entries: StackLayoutEntry[];
|
|
21
|
+
protected readonly gap: number;
|
|
22
|
+
protected readonly align: StackAlignment;
|
|
23
|
+
protected abstract readonly layoutType: StackLayoutType;
|
|
24
|
+
constructor(children?: StackChild[], options?: StackOptions);
|
|
25
|
+
addChild(component: Component, options?: StackEntryOptions): void;
|
|
26
|
+
removeChild(component: Component): void;
|
|
27
|
+
clear(): void;
|
|
28
|
+
[LAYOUT_NODE](): StackLayoutNode;
|
|
29
|
+
}
|
|
30
|
+
export declare function visibleStackEntries(entries: readonly StackLayoutEntry[], viewport: LayoutViewport): StackLayoutEntry[];
|
|
31
|
+
export declare function allocateStackSizes(entries: readonly StackLayoutEntry[], intrinsicSizes: readonly number[], availableSize: number | undefined, gap: number): number[];
|
|
32
|
+
//# sourceMappingURL=stack.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stack.d.ts","sourceRoot":"","sources":["../../src/components/stack.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,WAAW,EACX,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,KAAK,SAAS,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAUtD,MAAM,WAAW,iBAAiB;IACjC,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,cAAc,KAAK,OAAO,CAAC;CAChD;AAED,MAAM,WAAW,UAAW,SAAQ,iBAAiB;IACpD,SAAS,EAAE,SAAS,CAAC;CACrB;AAED,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,UAAU,CAAC;AAEhD,MAAM,WAAW,YAAY;IAC5B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,cAAc,CAAC;CACvB;AAUD,8BAAsB,KAAM,SAAQ,SAAS;IAC5C,SAAS,CAAC,QAAQ,CAAC,OAAO,EAAE,gBAAgB,EAAE,CAAM;IACpD,SAAS,CAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IAC/B,SAAS,CAAC,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAC;IACzC,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC,UAAU,EAAE,eAAe,CAAC;IAExD,YAAY,QAAQ,GAAE,UAAU,EAAO,EAAE,OAAO,GAAE,YAAiB,EAQlE;IAEQ,QAAQ,CAAC,SAAS,EAAE,SAAS,EAAE,OAAO,GAAE,iBAAsB,GAAG,IAAI,CAW7E;IAEQ,WAAW,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI,CAI/C;IAEQ,KAAK,IAAI,IAAI,CAGrB;IAED,CAAC,WAAW,CAAC,IAAI,eAAe,CAO/B;CACD;AAED,wBAAgB,mBAAmB,CAClC,OAAO,EAAE,SAAS,gBAAgB,EAAE,EACpC,QAAQ,EAAE,cAAc,GACtB,gBAAgB,EAAE,CAEpB;AAqED,wBAAgB,kBAAkB,CACjC,OAAO,EAAE,SAAS,gBAAgB,EAAE,EACpC,cAAc,EAAE,SAAS,MAAM,EAAE,EACjC,aAAa,EAAE,MAAM,GAAG,SAAS,EACjC,GAAG,EAAE,MAAM,GACT,MAAM,EAAE,CAcV","sourcesContent":["import {\n\tLAYOUT_NODE,\n\ttype LayoutViewport,\n\ttype StackAlignment,\n\ttype StackLayoutEntry,\n\ttype StackLayoutNode,\n\ttype StackLayoutType,\n} from \"../layout-node.ts\";\nimport { type Component, Container } from \"../tui.ts\";\n\ntype StackDistributionMode = \"grow\" | \"shrink\";\n\ninterface StackDistributionCandidate {\n\tindex: number;\n\tweight: number;\n\tcapacity: number;\n}\n\nexport interface StackEntryOptions {\n\tbasis?: number | \"auto\";\n\tgrow?: number;\n\tshrink?: number;\n\tminSize?: number;\n\tmaxSize?: number;\n\tvisible?: (viewport: LayoutViewport) => boolean;\n}\n\nexport interface StackEntry extends StackEntryOptions {\n\tcomponent: Component;\n}\n\nexport type StackChild = Component | StackEntry;\n\nexport interface StackOptions {\n\tgap?: number;\n\talign?: StackAlignment;\n}\n\nfunction isStackEntry(child: StackChild): child is StackEntry {\n\treturn !(\"render\" in child);\n}\n\nfunction normalizeSize(value: number | undefined, fallback: number): number {\n\treturn value === undefined || !Number.isFinite(value) ? fallback : Math.max(0, Math.floor(value));\n}\n\nexport abstract class Stack extends Container {\n\tprotected readonly entries: StackLayoutEntry[] = [];\n\tprotected readonly gap: number;\n\tprotected readonly align: StackAlignment;\n\tprotected abstract readonly layoutType: StackLayoutType;\n\n\tconstructor(children: StackChild[] = [], options: StackOptions = {}) {\n\t\tsuper();\n\t\tthis.gap = normalizeSize(options.gap, 0);\n\t\tthis.align = options.align ?? \"stretch\";\n\t\tfor (const child of children) {\n\t\t\tif (isStackEntry(child)) this.addChild(child.component, child);\n\t\t\telse this.addChild(child);\n\t\t}\n\t}\n\n\toverride addChild(component: Component, options: StackEntryOptions = {}): void {\n\t\tsuper.addChild(component);\n\t\tthis.entries.push({\n\t\t\tcomponent,\n\t\t\t...(options.basis === undefined ? {} : { basis: options.basis }),\n\t\t\t...(options.grow === undefined ? {} : { grow: normalizeSize(options.grow, 0) }),\n\t\t\t...(options.shrink === undefined ? {} : { shrink: normalizeSize(options.shrink, 1) }),\n\t\t\t...(options.minSize === undefined ? {} : { minSize: normalizeSize(options.minSize, 0) }),\n\t\t\t...(options.maxSize === undefined ? {} : { maxSize: normalizeSize(options.maxSize, Number.MAX_SAFE_INTEGER) }),\n\t\t\t...(options.visible === undefined ? {} : { visible: options.visible }),\n\t\t});\n\t}\n\n\toverride removeChild(component: Component): void {\n\t\tsuper.removeChild(component);\n\t\tconst index = this.entries.findIndex((entry) => entry.component === component);\n\t\tif (index !== -1) this.entries.splice(index, 1);\n\t}\n\n\toverride clear(): void {\n\t\tsuper.clear();\n\t\tthis.entries.length = 0;\n\t}\n\n\t[LAYOUT_NODE](): StackLayoutNode {\n\t\treturn {\n\t\t\ttype: this.layoutType,\n\t\t\tentries: this.entries,\n\t\t\tgap: this.gap,\n\t\t\talign: this.align,\n\t\t};\n\t}\n}\n\nexport function visibleStackEntries(\n\tentries: readonly StackLayoutEntry[],\n\tviewport: LayoutViewport,\n): StackLayoutEntry[] {\n\treturn entries.filter((entry) => entry.visible?.(viewport) ?? true);\n}\n\nfunction clampSize(size: number, entry: StackLayoutEntry): number {\n\tconst min = Math.max(0, Math.floor(entry.minSize ?? 0));\n\tconst max = Math.max(min, Math.floor(entry.maxSize ?? Number.MAX_SAFE_INTEGER));\n\treturn Math.max(min, Math.min(max, Math.max(0, Math.floor(size))));\n}\n\nfunction getDistributionWeight(entry: StackLayoutEntry, size: number, mode: StackDistributionMode): number {\n\treturn mode === \"grow\" ? (entry.grow ?? 0) : (entry.shrink ?? 1) * Math.max(1, size);\n}\n\nfunction getDistributionCapacity(entry: StackLayoutEntry, size: number, mode: StackDistributionMode): number {\n\treturn mode === \"grow\" ? (entry.maxSize ?? Number.MAX_SAFE_INTEGER) - size : size - (entry.minSize ?? 0);\n}\n\nfunction collectDistributionCandidates(\n\tsizes: number[],\n\tentries: readonly StackLayoutEntry[],\n\tmode: StackDistributionMode,\n): StackDistributionCandidate[] {\n\tconst candidates: StackDistributionCandidate[] = [];\n\tfor (let index = 0; index < entries.length; index++) {\n\t\tconst entry = entries[index]!;\n\t\tconst size = sizes[index]!;\n\t\tconst weight = getDistributionWeight(entry, size, mode);\n\t\tconst capacity = getDistributionCapacity(entry, size, mode);\n\t\tif (weight > 0 && capacity > 0) candidates.push({ index, weight, capacity });\n\t}\n\treturn candidates;\n}\n\nfunction distributeStackSizeRound(\n\tsizes: number[],\n\tcandidates: StackDistributionCandidate[],\n\tamount: number,\n\tmode: StackDistributionMode,\n): number {\n\tconst totalWeight = candidates.reduce((sum, candidate) => sum + candidate.weight, 0);\n\tlet remaining = amount;\n\tlet distributed = 0;\n\tfor (const candidate of candidates) {\n\t\tif (remaining <= 0) break;\n\t\tconst proposed = Math.max(1, Math.floor((remaining * candidate.weight) / totalWeight));\n\t\tconst delta = Math.min(remaining, proposed, candidate.capacity);\n\t\tif (delta <= 0) continue;\n\t\tsizes[candidate.index] = sizes[candidate.index]! + (mode === \"grow\" ? delta : -delta);\n\t\tremaining -= delta;\n\t\tdistributed += delta;\n\t}\n\treturn distributed;\n}\n\nfunction distribute(\n\tsizes: number[],\n\tentries: readonly StackLayoutEntry[],\n\tamount: number,\n\tmode: StackDistributionMode,\n): void {\n\tlet remaining = amount;\n\twhile (remaining > 0) {\n\t\tconst candidates = collectDistributionCandidates(sizes, entries, mode);\n\t\tif (candidates.length === 0) return;\n\t\tconst distributed = distributeStackSizeRound(sizes, candidates, remaining, mode);\n\t\tif (distributed === 0) return;\n\t\tremaining -= distributed;\n\t}\n}\n\nexport function allocateStackSizes(\n\tentries: readonly StackLayoutEntry[],\n\tintrinsicSizes: readonly number[],\n\tavailableSize: number | undefined,\n\tgap: number,\n): number[] {\n\tconst sizes = entries.map((entry, index) =>\n\t\tclampSize(\n\t\t\tentry.basis === undefined || entry.basis === \"auto\" ? (intrinsicSizes[index] ?? 0) : entry.basis,\n\t\t\tentry,\n\t\t),\n\t);\n\tif (availableSize === undefined) return sizes;\n\n\tconst contentSize = Math.max(0, Math.floor(availableSize) - Math.max(0, entries.length - 1) * gap);\n\tconst total = sizes.reduce((sum, size) => sum + size, 0);\n\tif (total < contentSize) distribute(sizes, entries, contentSize - total, \"grow\");\n\telse if (total > contentSize) distribute(sizes, entries, total - contentSize, \"shrink\");\n\treturn sizes;\n}\n"]}
|