@juliusbrussee/caveman-tui 0.65.2
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 +767 -0
- package/dist/autocomplete.d.ts +52 -0
- package/dist/autocomplete.d.ts.map +1 -0
- package/dist/autocomplete.js +623 -0
- package/dist/autocomplete.js.map +1 -0
- package/dist/chord.d.ts +57 -0
- package/dist/chord.d.ts.map +1 -0
- package/dist/chord.js +97 -0
- package/dist/chord.js.map +1 -0
- package/dist/color-depth.d.ts +17 -0
- package/dist/color-depth.d.ts.map +1 -0
- package/dist/color-depth.js +147 -0
- package/dist/color-depth.js.map +1 -0
- package/dist/components/Chapters.d.ts +41 -0
- package/dist/components/Chapters.d.ts.map +1 -0
- package/dist/components/Chapters.js +103 -0
- package/dist/components/Chapters.js.map +1 -0
- package/dist/components/DiffView.d.ts +75 -0
- package/dist/components/DiffView.d.ts.map +1 -0
- package/dist/components/DiffView.js +170 -0
- package/dist/components/DiffView.js.map +1 -0
- package/dist/components/StatusLine.d.ts +135 -0
- package/dist/components/StatusLine.d.ts.map +1 -0
- package/dist/components/StatusLine.js +133 -0
- package/dist/components/StatusLine.js.map +1 -0
- package/dist/components/SubagentOverlay.d.ts +63 -0
- package/dist/components/SubagentOverlay.d.ts.map +1 -0
- package/dist/components/SubagentOverlay.js +124 -0
- package/dist/components/SubagentOverlay.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 +244 -0
- package/dist/components/editor.d.ts.map +1 -0
- package/dist/components/editor.js +1861 -0
- package/dist/components/editor.js.map +1 -0
- package/dist/components/grouped-select-list.d.ts +60 -0
- package/dist/components/grouped-select-list.d.ts.map +1 -0
- package/dist/components/grouped-select-list.js +312 -0
- package/dist/components/grouped-select-list.js.map +1 -0
- package/dist/components/image.d.ts +28 -0
- package/dist/components/image.d.ts.map +1 -0
- package/dist/components/image.js +69 -0
- package/dist/components/image.js.map +1 -0
- package/dist/components/input.d.ts +37 -0
- package/dist/components/input.d.ts.map +1 -0
- package/dist/components/input.js +426 -0
- package/dist/components/input.js.map +1 -0
- package/dist/components/loader.d.ts +26 -0
- package/dist/components/loader.d.ts.map +1 -0
- package/dist/components/loader.js +67 -0
- package/dist/components/loader.js.map +1 -0
- package/dist/components/markdown.d.ts +95 -0
- package/dist/components/markdown.d.ts.map +1 -0
- package/dist/components/markdown.js +663 -0
- package/dist/components/markdown.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 +50 -0
- package/dist/components/settings-list.d.ts.map +1 -0
- package/dist/components/settings-list.js +185 -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/spinner.d.ts +35 -0
- package/dist/components/spinner.d.ts.map +1 -0
- package/dist/components/spinner.js +77 -0
- package/dist/components/spinner.js.map +1 -0
- package/dist/components/streaming-markdown.d.ts +39 -0
- package/dist/components/streaming-markdown.d.ts.map +1 -0
- package/dist/components/streaming-markdown.js +137 -0
- package/dist/components/streaming-markdown.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/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 +107 -0
- package/dist/fuzzy.js.map +1 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +59 -0
- package/dist/index.js.map +1 -0
- package/dist/keybindings.d.ts +193 -0
- package/dist/keybindings.d.ts.map +1 -0
- package/dist/keybindings.js +174 -0
- package/dist/keybindings.js.map +1 -0
- package/dist/keys.d.ts +170 -0
- package/dist/keys.d.ts.map +1 -0
- package/dist/keys.js +1124 -0
- package/dist/keys.js.map +1 -0
- package/dist/kill-ring.d.ts +28 -0
- package/dist/kill-ring.d.ts.map +1 -0
- package/dist/kill-ring.js +44 -0
- package/dist/kill-ring.js.map +1 -0
- package/dist/notifications.d.ts +35 -0
- package/dist/notifications.d.ts.map +1 -0
- package/dist/notifications.js +62 -0
- package/dist/notifications.js.map +1 -0
- package/dist/osc52.d.ts +28 -0
- package/dist/osc52.d.ts.map +1 -0
- package/dist/osc52.js +53 -0
- package/dist/osc52.js.map +1 -0
- package/dist/scroll-buffer.d.ts +67 -0
- package/dist/scroll-buffer.d.ts.map +1 -0
- package/dist/scroll-buffer.js +222 -0
- package/dist/scroll-buffer.js.map +1 -0
- package/dist/spinners.d.ts +26 -0
- package/dist/spinners.d.ts.map +1 -0
- package/dist/spinners.js +136 -0
- package/dist/spinners.js.map +1 -0
- package/dist/stdin-buffer.d.ts +48 -0
- package/dist/stdin-buffer.d.ts.map +1 -0
- package/dist/stdin-buffer.js +317 -0
- package/dist/stdin-buffer.js.map +1 -0
- package/dist/sync-output.d.ts +58 -0
- package/dist/sync-output.d.ts.map +1 -0
- package/dist/sync-output.js +79 -0
- package/dist/sync-output.js.map +1 -0
- package/dist/terminal-detect.d.ts +66 -0
- package/dist/terminal-detect.d.ts.map +1 -0
- package/dist/terminal-detect.js +315 -0
- package/dist/terminal-detect.js.map +1 -0
- package/dist/terminal-image.d.ts +68 -0
- package/dist/terminal-image.d.ts.map +1 -0
- package/dist/terminal-image.js +288 -0
- package/dist/terminal-image.js.map +1 -0
- package/dist/terminal.d.ts +105 -0
- package/dist/terminal.d.ts.map +1 -0
- package/dist/terminal.js +427 -0
- package/dist/terminal.js.map +1 -0
- package/dist/tui.d.ts +268 -0
- package/dist/tui.d.ts.map +1 -0
- package/dist/tui.js +1161 -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 +78 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +960 -0
- package/dist/utils.js.map +1 -0
- package/package.json +59 -0
|
@@ -0,0 +1,185 @@
|
|
|
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
|
+
// Add description for selected item
|
|
95
|
+
const selectedItem = displayItems[this.selectedIndex];
|
|
96
|
+
if (selectedItem?.description) {
|
|
97
|
+
lines.push("");
|
|
98
|
+
const wrappedDesc = wrapTextWithAnsi(selectedItem.description, width - 4);
|
|
99
|
+
for (const line of wrappedDesc) {
|
|
100
|
+
lines.push(this.theme.description(` ${line}`));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// Add hint
|
|
104
|
+
this.addHintLine(lines, width);
|
|
105
|
+
return lines;
|
|
106
|
+
}
|
|
107
|
+
handleInput(data) {
|
|
108
|
+
// If submenu is active, delegate all input to it
|
|
109
|
+
// The submenu's onCancel (triggered by escape) will call done() which closes it
|
|
110
|
+
if (this.submenuComponent) {
|
|
111
|
+
this.submenuComponent.handleInput?.(data);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
// Main list input handling
|
|
115
|
+
const kb = getKeybindings();
|
|
116
|
+
const displayItems = this.searchEnabled ? this.filteredItems : this.items;
|
|
117
|
+
if (kb.matches(data, "tui.select.up")) {
|
|
118
|
+
if (displayItems.length === 0)
|
|
119
|
+
return;
|
|
120
|
+
this.selectedIndex = this.selectedIndex === 0 ? displayItems.length - 1 : this.selectedIndex - 1;
|
|
121
|
+
}
|
|
122
|
+
else if (kb.matches(data, "tui.select.down")) {
|
|
123
|
+
if (displayItems.length === 0)
|
|
124
|
+
return;
|
|
125
|
+
this.selectedIndex = this.selectedIndex === displayItems.length - 1 ? 0 : this.selectedIndex + 1;
|
|
126
|
+
}
|
|
127
|
+
else if (kb.matches(data, "tui.select.confirm") || data === " ") {
|
|
128
|
+
this.activateItem();
|
|
129
|
+
}
|
|
130
|
+
else if (kb.matches(data, "tui.select.cancel")) {
|
|
131
|
+
this.onCancel();
|
|
132
|
+
}
|
|
133
|
+
else if (this.searchEnabled && this.searchInput) {
|
|
134
|
+
const sanitized = data.replace(/ /g, "");
|
|
135
|
+
if (!sanitized) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
this.searchInput.handleInput(sanitized);
|
|
139
|
+
this.applyFilter(this.searchInput.getValue());
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
activateItem() {
|
|
143
|
+
const item = this.searchEnabled ? this.filteredItems[this.selectedIndex] : this.items[this.selectedIndex];
|
|
144
|
+
if (!item)
|
|
145
|
+
return;
|
|
146
|
+
if (item.submenu) {
|
|
147
|
+
// Open submenu, passing current value so it can pre-select correctly
|
|
148
|
+
this.submenuItemIndex = this.selectedIndex;
|
|
149
|
+
this.submenuComponent = item.submenu(item.currentValue, (selectedValue) => {
|
|
150
|
+
if (selectedValue !== undefined) {
|
|
151
|
+
item.currentValue = selectedValue;
|
|
152
|
+
this.onChange(item.id, selectedValue);
|
|
153
|
+
}
|
|
154
|
+
this.closeSubmenu();
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
else if (item.values && item.values.length > 0) {
|
|
158
|
+
// Cycle through values
|
|
159
|
+
const currentIndex = item.values.indexOf(item.currentValue);
|
|
160
|
+
const nextIndex = (currentIndex + 1) % item.values.length;
|
|
161
|
+
const newValue = item.values[nextIndex];
|
|
162
|
+
item.currentValue = newValue;
|
|
163
|
+
this.onChange(item.id, newValue);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
closeSubmenu() {
|
|
167
|
+
this.submenuComponent = null;
|
|
168
|
+
// Restore selection to the item that opened the submenu
|
|
169
|
+
if (this.submenuItemIndex !== null) {
|
|
170
|
+
this.selectedIndex = this.submenuItemIndex;
|
|
171
|
+
this.submenuItemIndex = null;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
applyFilter(query) {
|
|
175
|
+
this.filteredItems = fuzzyFilter(this.items, query, (item) => item.label);
|
|
176
|
+
this.selectedIndex = 0;
|
|
177
|
+
}
|
|
178
|
+
addHintLine(lines, width) {
|
|
179
|
+
lines.push("");
|
|
180
|
+
lines.push(truncateToWidth(this.theme.hint(this.searchEnabled
|
|
181
|
+
? " Type to search · Enter/Space to change · Esc to cancel"
|
|
182
|
+
: " Enter/Space to change · Esc to cancel"), width));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
//# sourceMappingURL=settings-list.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settings-list.js","sourceRoot":"","sources":["../../src/components/settings-list.ts"],"names":[],"mappings":"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","sourcesContent":["import { fuzzyFilter } from \"../fuzzy.js\";\nimport { getKeybindings } from \"../keybindings.js\";\nimport type { Component } from \"../tui.js\";\nimport { truncateToWidth, visibleWidth, wrapTextWithAnsi } from \"../utils.js\";\nimport { Input } from \"./input.js\";\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\t// Add description for selected item\n\t\tconst selectedItem = displayItems[this.selectedIndex];\n\t\tif (selectedItem?.description) {\n\t\t\tlines.push(\"\");\n\t\t\tconst wrappedDesc = wrapTextWithAnsi(selectedItem.description, width - 4);\n\t\t\tfor (const line of wrappedDesc) {\n\t\t\t\tlines.push(this.theme.description(` ${line}`));\n\t\t\t}\n\t\t}\n\n\t\t// Add hint\n\t\tthis.addHintLine(lines, width);\n\n\t\treturn lines;\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\t// Main list input handling\n\t\tconst kb = getKeybindings();\n\t\tconst displayItems = this.searchEnabled ? this.filteredItems : this.items;\n\t\tif (kb.matches(data, \"tui.select.up\")) {\n\t\t\tif (displayItems.length === 0) return;\n\t\t\tthis.selectedIndex = this.selectedIndex === 0 ? displayItems.length - 1 : this.selectedIndex - 1;\n\t\t} else if (kb.matches(data, \"tui.select.down\")) {\n\t\t\tif (displayItems.length === 0) return;\n\t\t\tthis.selectedIndex = this.selectedIndex === displayItems.length - 1 ? 0 : this.selectedIndex + 1;\n\t\t} else if (kb.matches(data, \"tui.select.confirm\") || data === \" \") {\n\t\t\tthis.activateItem();\n\t\t} else if (kb.matches(data, \"tui.select.cancel\")) {\n\t\t\tthis.onCancel();\n\t\t} else if (this.searchEnabled && this.searchInput) {\n\t\t\tconst sanitized = data.replace(/ /g, \"\");\n\t\t\tif (!sanitized) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthis.searchInput.handleInput(sanitized);\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.js";
|
|
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.js\";\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.js\";\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,35 @@
|
|
|
1
|
+
import { type SpinnerName, type SpinnerVariant } from "../spinners.js";
|
|
2
|
+
import type { TUI } from "../tui.js";
|
|
3
|
+
import { Text } from "./text.js";
|
|
4
|
+
export interface SpinnerOptions {
|
|
5
|
+
variant?: SpinnerName | SpinnerVariant;
|
|
6
|
+
colorFn?: (text: string) => string;
|
|
7
|
+
message?: string;
|
|
8
|
+
messageColorFn?: (text: string) => string;
|
|
9
|
+
paddingX?: number;
|
|
10
|
+
paddingY?: number;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Standalone spinner that drives its own animation timer.
|
|
14
|
+
*
|
|
15
|
+
* Subclasses Text so it composes inside any Container that already renders Text.
|
|
16
|
+
* Call dispose() (or removeChild on the parent) to release the timer.
|
|
17
|
+
*/
|
|
18
|
+
export declare class Spinner extends Text {
|
|
19
|
+
private spinner;
|
|
20
|
+
private colorFn;
|
|
21
|
+
private messageColorFn;
|
|
22
|
+
private message;
|
|
23
|
+
private currentFrame;
|
|
24
|
+
private intervalId;
|
|
25
|
+
private ui;
|
|
26
|
+
constructor(ui: TUI | null, options?: SpinnerOptions);
|
|
27
|
+
setVariant(variant: SpinnerName | SpinnerVariant): void;
|
|
28
|
+
setMessage(message: string): void;
|
|
29
|
+
setColorFn(fn: (text: string) => string): void;
|
|
30
|
+
start(): void;
|
|
31
|
+
stop(): void;
|
|
32
|
+
dispose(): void;
|
|
33
|
+
private updateDisplay;
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=spinner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spinner.d.ts","sourceRoot":"","sources":["../../src/components/spinner.ts"],"names":[],"mappings":"AAAA,OAAO,EAAwB,KAAK,WAAW,EAAE,KAAK,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAC7F,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AACrC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,MAAM,WAAW,cAAc;IAC9B,OAAO,CAAC,EAAE,WAAW,GAAG,cAAc,CAAC;IACvC,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IACnC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IAC1C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;;GAKG;AACH,qBAAa,OAAQ,SAAQ,IAAI;IAChC,OAAO,CAAC,OAAO,CAAiB;IAChC,OAAO,CAAC,OAAO,CAA2B;IAC1C,OAAO,CAAC,cAAc,CAA2B;IACjD,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,UAAU,CAA+B;IACjD,OAAO,CAAC,EAAE,CAAa;IAEvB,YAAY,EAAE,EAAE,GAAG,GAAG,IAAI,EAAE,OAAO,GAAE,cAAmB,EAQvD;IAED,UAAU,CAAC,OAAO,EAAE,WAAW,GAAG,cAAc,GAAG,IAAI,CAKtD;IAED,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAGhC;IAED,UAAU,CAAC,EAAE,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAG7C;IAED,KAAK,IAAI,IAAI,CAOZ;IAED,IAAI,IAAI,IAAI,CAKX;IAED,OAAO,IAAI,IAAI,CAEd;IAED,OAAO,CAAC,aAAa;CAQrB","sourcesContent":["import { getSpinner, SPINNERS, type SpinnerName, type SpinnerVariant } from \"../spinners.js\";\nimport type { TUI } from \"../tui.js\";\nimport { Text } from \"./text.js\";\n\nexport interface SpinnerOptions {\n\tvariant?: SpinnerName | SpinnerVariant;\n\tcolorFn?: (text: string) => string;\n\tmessage?: string;\n\tmessageColorFn?: (text: string) => string;\n\tpaddingX?: number;\n\tpaddingY?: number;\n}\n\n/**\n * Standalone spinner that drives its own animation timer.\n *\n * Subclasses Text so it composes inside any Container that already renders Text.\n * Call dispose() (or removeChild on the parent) to release the timer.\n */\nexport class Spinner extends Text {\n\tprivate spinner: SpinnerVariant;\n\tprivate colorFn: (text: string) => string;\n\tprivate messageColorFn: (text: string) => string;\n\tprivate message: string;\n\tprivate currentFrame = 0;\n\tprivate intervalId: NodeJS.Timeout | null = null;\n\tprivate ui: TUI | null;\n\n\tconstructor(ui: TUI | null, options: SpinnerOptions = {}) {\n\t\tsuper(\"\", options.paddingX ?? 1, options.paddingY ?? 0);\n\t\tthis.ui = ui;\n\t\tthis.spinner = resolveVariant(options.variant);\n\t\tthis.colorFn = options.colorFn ?? identity;\n\t\tthis.messageColorFn = options.messageColorFn ?? identity;\n\t\tthis.message = options.message ?? \"\";\n\t\tthis.start();\n\t}\n\n\tsetVariant(variant: SpinnerName | SpinnerVariant): void {\n\t\tthis.stop();\n\t\tthis.spinner = resolveVariant(variant);\n\t\tthis.currentFrame = 0;\n\t\tthis.start();\n\t}\n\n\tsetMessage(message: string): void {\n\t\tthis.message = message;\n\t\tthis.updateDisplay();\n\t}\n\n\tsetColorFn(fn: (text: string) => string): void {\n\t\tthis.colorFn = fn;\n\t\tthis.updateDisplay();\n\t}\n\n\tstart(): void {\n\t\tif (this.intervalId) return;\n\t\tthis.updateDisplay();\n\t\tthis.intervalId = setInterval(() => {\n\t\t\tthis.currentFrame = (this.currentFrame + 1) % this.spinner.frames.length;\n\t\t\tthis.updateDisplay();\n\t\t}, this.spinner.interval);\n\t}\n\n\tstop(): void {\n\t\tif (this.intervalId) {\n\t\t\tclearInterval(this.intervalId);\n\t\t\tthis.intervalId = null;\n\t\t}\n\t}\n\n\tdispose(): void {\n\t\tthis.stop();\n\t}\n\n\tprivate updateDisplay(): void {\n\t\tconst frame = this.spinner.frames[this.currentFrame];\n\t\tconst composed = this.message\n\t\t\t? `${this.colorFn(frame)} ${this.messageColorFn(this.message)}`\n\t\t\t: this.colorFn(frame);\n\t\tthis.setText(composed);\n\t\tthis.ui?.requestRender();\n\t}\n}\n\nfunction identity(text: string): string {\n\treturn text;\n}\n\nfunction resolveVariant(input?: SpinnerName | SpinnerVariant): SpinnerVariant {\n\tif (!input) return SPINNERS.breathe;\n\tif (typeof input === \"string\") return getSpinner(input, \"breathe\");\n\treturn input;\n}\n"]}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { getSpinner, SPINNERS } from "../spinners.js";
|
|
2
|
+
import { Text } from "./text.js";
|
|
3
|
+
/**
|
|
4
|
+
* Standalone spinner that drives its own animation timer.
|
|
5
|
+
*
|
|
6
|
+
* Subclasses Text so it composes inside any Container that already renders Text.
|
|
7
|
+
* Call dispose() (or removeChild on the parent) to release the timer.
|
|
8
|
+
*/
|
|
9
|
+
export class Spinner extends Text {
|
|
10
|
+
spinner;
|
|
11
|
+
colorFn;
|
|
12
|
+
messageColorFn;
|
|
13
|
+
message;
|
|
14
|
+
currentFrame = 0;
|
|
15
|
+
intervalId = null;
|
|
16
|
+
ui;
|
|
17
|
+
constructor(ui, options = {}) {
|
|
18
|
+
super("", options.paddingX ?? 1, options.paddingY ?? 0);
|
|
19
|
+
this.ui = ui;
|
|
20
|
+
this.spinner = resolveVariant(options.variant);
|
|
21
|
+
this.colorFn = options.colorFn ?? identity;
|
|
22
|
+
this.messageColorFn = options.messageColorFn ?? identity;
|
|
23
|
+
this.message = options.message ?? "";
|
|
24
|
+
this.start();
|
|
25
|
+
}
|
|
26
|
+
setVariant(variant) {
|
|
27
|
+
this.stop();
|
|
28
|
+
this.spinner = resolveVariant(variant);
|
|
29
|
+
this.currentFrame = 0;
|
|
30
|
+
this.start();
|
|
31
|
+
}
|
|
32
|
+
setMessage(message) {
|
|
33
|
+
this.message = message;
|
|
34
|
+
this.updateDisplay();
|
|
35
|
+
}
|
|
36
|
+
setColorFn(fn) {
|
|
37
|
+
this.colorFn = fn;
|
|
38
|
+
this.updateDisplay();
|
|
39
|
+
}
|
|
40
|
+
start() {
|
|
41
|
+
if (this.intervalId)
|
|
42
|
+
return;
|
|
43
|
+
this.updateDisplay();
|
|
44
|
+
this.intervalId = setInterval(() => {
|
|
45
|
+
this.currentFrame = (this.currentFrame + 1) % this.spinner.frames.length;
|
|
46
|
+
this.updateDisplay();
|
|
47
|
+
}, this.spinner.interval);
|
|
48
|
+
}
|
|
49
|
+
stop() {
|
|
50
|
+
if (this.intervalId) {
|
|
51
|
+
clearInterval(this.intervalId);
|
|
52
|
+
this.intervalId = null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
dispose() {
|
|
56
|
+
this.stop();
|
|
57
|
+
}
|
|
58
|
+
updateDisplay() {
|
|
59
|
+
const frame = this.spinner.frames[this.currentFrame];
|
|
60
|
+
const composed = this.message
|
|
61
|
+
? `${this.colorFn(frame)} ${this.messageColorFn(this.message)}`
|
|
62
|
+
: this.colorFn(frame);
|
|
63
|
+
this.setText(composed);
|
|
64
|
+
this.ui?.requestRender();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function identity(text) {
|
|
68
|
+
return text;
|
|
69
|
+
}
|
|
70
|
+
function resolveVariant(input) {
|
|
71
|
+
if (!input)
|
|
72
|
+
return SPINNERS.breathe;
|
|
73
|
+
if (typeof input === "string")
|
|
74
|
+
return getSpinner(input, "breathe");
|
|
75
|
+
return input;
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=spinner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spinner.js","sourceRoot":"","sources":["../../src/components/spinner.ts"],"names":[],"mappings":"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","sourcesContent":["import { getSpinner, SPINNERS, type SpinnerName, type SpinnerVariant } from \"../spinners.js\";\nimport type { TUI } from \"../tui.js\";\nimport { Text } from \"./text.js\";\n\nexport interface SpinnerOptions {\n\tvariant?: SpinnerName | SpinnerVariant;\n\tcolorFn?: (text: string) => string;\n\tmessage?: string;\n\tmessageColorFn?: (text: string) => string;\n\tpaddingX?: number;\n\tpaddingY?: number;\n}\n\n/**\n * Standalone spinner that drives its own animation timer.\n *\n * Subclasses Text so it composes inside any Container that already renders Text.\n * Call dispose() (or removeChild on the parent) to release the timer.\n */\nexport class Spinner extends Text {\n\tprivate spinner: SpinnerVariant;\n\tprivate colorFn: (text: string) => string;\n\tprivate messageColorFn: (text: string) => string;\n\tprivate message: string;\n\tprivate currentFrame = 0;\n\tprivate intervalId: NodeJS.Timeout | null = null;\n\tprivate ui: TUI | null;\n\n\tconstructor(ui: TUI | null, options: SpinnerOptions = {}) {\n\t\tsuper(\"\", options.paddingX ?? 1, options.paddingY ?? 0);\n\t\tthis.ui = ui;\n\t\tthis.spinner = resolveVariant(options.variant);\n\t\tthis.colorFn = options.colorFn ?? identity;\n\t\tthis.messageColorFn = options.messageColorFn ?? identity;\n\t\tthis.message = options.message ?? \"\";\n\t\tthis.start();\n\t}\n\n\tsetVariant(variant: SpinnerName | SpinnerVariant): void {\n\t\tthis.stop();\n\t\tthis.spinner = resolveVariant(variant);\n\t\tthis.currentFrame = 0;\n\t\tthis.start();\n\t}\n\n\tsetMessage(message: string): void {\n\t\tthis.message = message;\n\t\tthis.updateDisplay();\n\t}\n\n\tsetColorFn(fn: (text: string) => string): void {\n\t\tthis.colorFn = fn;\n\t\tthis.updateDisplay();\n\t}\n\n\tstart(): void {\n\t\tif (this.intervalId) return;\n\t\tthis.updateDisplay();\n\t\tthis.intervalId = setInterval(() => {\n\t\t\tthis.currentFrame = (this.currentFrame + 1) % this.spinner.frames.length;\n\t\t\tthis.updateDisplay();\n\t\t}, this.spinner.interval);\n\t}\n\n\tstop(): void {\n\t\tif (this.intervalId) {\n\t\t\tclearInterval(this.intervalId);\n\t\t\tthis.intervalId = null;\n\t\t}\n\t}\n\n\tdispose(): void {\n\t\tthis.stop();\n\t}\n\n\tprivate updateDisplay(): void {\n\t\tconst frame = this.spinner.frames[this.currentFrame];\n\t\tconst composed = this.message\n\t\t\t? `${this.colorFn(frame)} ${this.messageColorFn(this.message)}`\n\t\t\t: this.colorFn(frame);\n\t\tthis.setText(composed);\n\t\tthis.ui?.requestRender();\n\t}\n}\n\nfunction identity(text: string): string {\n\treturn text;\n}\n\nfunction resolveVariant(input?: SpinnerName | SpinnerVariant): SpinnerVariant {\n\tif (!input) return SPINNERS.breathe;\n\tif (typeof input === \"string\") return getSpinner(input, \"breathe\");\n\treturn input;\n}\n"]}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Component } from "../tui.js";
|
|
2
|
+
import { type DefaultTextStyle, type MarkdownTheme } from "./markdown.js";
|
|
3
|
+
/**
|
|
4
|
+
* Markdown renderer for chunked, in-flight assistant text.
|
|
5
|
+
*
|
|
6
|
+
* Wraps Markdown so partial content renders cleanly even while half-formed
|
|
7
|
+
* tokens (open fences, unfinished bold, dangling link brackets) are still
|
|
8
|
+
* arriving. Once the stream finalizes, swap in the canonical render via
|
|
9
|
+
* `finalize()`.
|
|
10
|
+
*/
|
|
11
|
+
export declare class StreamingMarkdown implements Component {
|
|
12
|
+
readonly paddingX: number;
|
|
13
|
+
readonly paddingY: number;
|
|
14
|
+
readonly theme: MarkdownTheme;
|
|
15
|
+
readonly defaultTextStyle?: DefaultTextStyle | undefined;
|
|
16
|
+
private inner;
|
|
17
|
+
private rawBuffer;
|
|
18
|
+
private finalized;
|
|
19
|
+
constructor(initial: string, paddingX: number, paddingY: number, theme: MarkdownTheme, defaultTextStyle?: DefaultTextStyle | undefined);
|
|
20
|
+
/** Append a streamed chunk and update the render. */
|
|
21
|
+
append(chunk: string): void;
|
|
22
|
+
/** Replace the entire buffer (e.g. when retrying). */
|
|
23
|
+
setText(text: string): void;
|
|
24
|
+
/**
|
|
25
|
+
* Mark the stream complete and swap to the canonical full markdown render.
|
|
26
|
+
* Subsequent appends (rare) keep the canonical render path.
|
|
27
|
+
*/
|
|
28
|
+
finalize(canonical?: string): void;
|
|
29
|
+
getRawText(): string;
|
|
30
|
+
invalidate(): void;
|
|
31
|
+
render(width: number): string[];
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Balance partial markdown so the in-flight render doesn't trip over
|
|
35
|
+
* unfinished syntax. Conservative: no semantic re-parsing, just close the
|
|
36
|
+
* obvious cases (fences, runs of ** / * / ~~, dangling [ ).
|
|
37
|
+
*/
|
|
38
|
+
export declare function balancePartial(text: string): string;
|
|
39
|
+
//# sourceMappingURL=streaming-markdown.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"streaming-markdown.d.ts","sourceRoot":"","sources":["../../src/components/streaming-markdown.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAC3C,OAAO,EAAE,KAAK,gBAAgB,EAAY,KAAK,aAAa,EAAE,MAAM,eAAe,CAAC;AAEpF;;;;;;;GAOG;AACH,qBAAa,iBAAkB,YAAW,SAAS;IAOjD,QAAQ,CAAC,QAAQ,EAAE,MAAM;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM;IACzB,QAAQ,CAAC,KAAK,EAAE,aAAa;IAC7B,QAAQ,CAAC,gBAAgB,CAAC;IAT3B,OAAO,CAAC,KAAK,CAAW;IACxB,OAAO,CAAC,SAAS,CAAM;IACvB,OAAO,CAAC,SAAS,CAAS;IAE1B,YACC,OAAO,EAAE,MAAM,EACN,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,aAAa,EACpB,gBAAgB,CAAC,8BAAkB,EAI5C;IAED,qDAAqD;IACrD,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAQ1B;IAED,sDAAsD;IACtD,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAO1B;IAED;;;OAGG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAMjC;IAED,UAAU,IAAI,MAAM,CAEnB;IAED,UAAU,IAAI,IAAI,CAEjB;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAE9B;CACD;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAsCnD","sourcesContent":["import type { Component } from \"../tui.js\";\nimport { type DefaultTextStyle, Markdown, type MarkdownTheme } from \"./markdown.js\";\n\n/**\n * Markdown renderer for chunked, in-flight assistant text.\n *\n * Wraps Markdown so partial content renders cleanly even while half-formed\n * tokens (open fences, unfinished bold, dangling link brackets) are still\n * arriving. Once the stream finalizes, swap in the canonical render via\n * `finalize()`.\n */\nexport class StreamingMarkdown implements Component {\n\tprivate inner: Markdown;\n\tprivate rawBuffer = \"\";\n\tprivate finalized = false;\n\n\tconstructor(\n\t\tinitial: string,\n\t\treadonly paddingX: number,\n\t\treadonly paddingY: number,\n\t\treadonly theme: MarkdownTheme,\n\t\treadonly defaultTextStyle?: DefaultTextStyle,\n\t) {\n\t\tthis.inner = new Markdown(balancePartial(initial), paddingX, paddingY, theme, defaultTextStyle);\n\t\tthis.rawBuffer = initial;\n\t}\n\n\t/** Append a streamed chunk and update the render. */\n\tappend(chunk: string): void {\n\t\tif (this.finalized) {\n\t\t\tthis.rawBuffer += chunk;\n\t\t\tthis.inner.setText(this.rawBuffer);\n\t\t\treturn;\n\t\t}\n\t\tthis.rawBuffer += chunk;\n\t\tthis.inner.setText(balancePartial(this.rawBuffer));\n\t}\n\n\t/** Replace the entire buffer (e.g. when retrying). */\n\tsetText(text: string): void {\n\t\tthis.rawBuffer = text;\n\t\tif (this.finalized) {\n\t\t\tthis.inner.setText(text);\n\t\t} else {\n\t\t\tthis.inner.setText(balancePartial(text));\n\t\t}\n\t}\n\n\t/**\n\t * Mark the stream complete and swap to the canonical full markdown render.\n\t * Subsequent appends (rare) keep the canonical render path.\n\t */\n\tfinalize(canonical?: string): void {\n\t\tthis.finalized = true;\n\t\tif (canonical !== undefined) {\n\t\t\tthis.rawBuffer = canonical;\n\t\t}\n\t\tthis.inner.setText(this.rawBuffer);\n\t}\n\n\tgetRawText(): string {\n\t\treturn this.rawBuffer;\n\t}\n\n\tinvalidate(): void {\n\t\tthis.inner.invalidate();\n\t}\n\n\trender(width: number): string[] {\n\t\treturn this.inner.render(width);\n\t}\n}\n\n/**\n * Balance partial markdown so the in-flight render doesn't trip over\n * unfinished syntax. Conservative: no semantic re-parsing, just close the\n * obvious cases (fences, runs of ** / * / ~~, dangling [ ).\n */\nexport function balancePartial(text: string): string {\n\tlet working = text;\n\n\t// Trim a trailing dangling link prefix like \"see [the docs\" or \"see [docs](\".\n\t// We only strip the unbalanced opening so the partial render shows the\n\t// preceding prose without an angry render of brackets.\n\tconst lastOpenBracket = working.lastIndexOf(\"[\");\n\tif (lastOpenBracket !== -1) {\n\t\tconst tail = working.slice(lastOpenBracket);\n\t\tconst closingBracket = tail.indexOf(\"]\");\n\t\tconst openParen = closingBracket !== -1 ? tail.indexOf(\"(\", closingBracket) : -1;\n\t\tconst closingParen = openParen !== -1 ? tail.indexOf(\")\", openParen) : -1;\n\t\tif (closingBracket === -1 || (openParen !== -1 && closingParen === -1)) {\n\t\t\tworking = working.slice(0, lastOpenBracket);\n\t\t}\n\t}\n\n\t// Close an open fenced code block. We count fence lines (``` at line start,\n\t// possibly with lang). Odd count → still inside a fence → append a closing\n\t// fence so the render terminates cleanly.\n\tconst fenceLines = working.split(\"\\n\").filter((line) => /^\\s{0,3}```/.test(line));\n\tif (fenceLines.length % 2 === 1) {\n\t\t// If the last fence opener is mid-line (no trailing newline), add one.\n\t\tif (!working.endsWith(\"\\n\")) working += \"\\n\";\n\t\tworking += \"```\\n\";\n\t}\n\n\t// Strip trailing odd-count strong markers ** that haven't been closed.\n\tif (countOutsideCode(working, \"**\") % 2 === 1) {\n\t\tworking = stripTrailingMarker(working, \"**\");\n\t}\n\t// Same for ~~ (strikethrough).\n\tif (countOutsideCode(working, \"~~\") % 2 === 1) {\n\t\tworking = stripTrailingMarker(working, \"~~\");\n\t}\n\t// Single * is ambiguous (lists, italics); leave it alone.\n\n\treturn working;\n}\n\nfunction countOutsideCode(text: string, marker: string): number {\n\t// Naive: count occurrences of `marker` outside backtick spans. Good enough\n\t// for the partial-balance use case — false positives only mean we leave a\n\t// run open, which the renderer already tolerates.\n\tlet count = 0;\n\tlet inCode = false;\n\tlet i = 0;\n\twhile (i < text.length) {\n\t\tconst ch = text[i];\n\t\tif (ch === \"`\") {\n\t\t\tinCode = !inCode;\n\t\t\ti += 1;\n\t\t\tcontinue;\n\t\t}\n\t\tif (!inCode && text.startsWith(marker, i)) {\n\t\t\tcount += 1;\n\t\t\ti += marker.length;\n\t\t\tcontinue;\n\t\t}\n\t\ti += 1;\n\t}\n\treturn count;\n}\n\nfunction stripTrailingMarker(text: string, marker: string): string {\n\tconst idx = text.lastIndexOf(marker);\n\tif (idx === -1) return text;\n\treturn text.slice(0, idx);\n}\n"]}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { Markdown } from "./markdown.js";
|
|
2
|
+
/**
|
|
3
|
+
* Markdown renderer for chunked, in-flight assistant text.
|
|
4
|
+
*
|
|
5
|
+
* Wraps Markdown so partial content renders cleanly even while half-formed
|
|
6
|
+
* tokens (open fences, unfinished bold, dangling link brackets) are still
|
|
7
|
+
* arriving. Once the stream finalizes, swap in the canonical render via
|
|
8
|
+
* `finalize()`.
|
|
9
|
+
*/
|
|
10
|
+
export class StreamingMarkdown {
|
|
11
|
+
paddingX;
|
|
12
|
+
paddingY;
|
|
13
|
+
theme;
|
|
14
|
+
defaultTextStyle;
|
|
15
|
+
inner;
|
|
16
|
+
rawBuffer = "";
|
|
17
|
+
finalized = false;
|
|
18
|
+
constructor(initial, paddingX, paddingY, theme, defaultTextStyle) {
|
|
19
|
+
this.paddingX = paddingX;
|
|
20
|
+
this.paddingY = paddingY;
|
|
21
|
+
this.theme = theme;
|
|
22
|
+
this.defaultTextStyle = defaultTextStyle;
|
|
23
|
+
this.inner = new Markdown(balancePartial(initial), paddingX, paddingY, theme, defaultTextStyle);
|
|
24
|
+
this.rawBuffer = initial;
|
|
25
|
+
}
|
|
26
|
+
/** Append a streamed chunk and update the render. */
|
|
27
|
+
append(chunk) {
|
|
28
|
+
if (this.finalized) {
|
|
29
|
+
this.rawBuffer += chunk;
|
|
30
|
+
this.inner.setText(this.rawBuffer);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
this.rawBuffer += chunk;
|
|
34
|
+
this.inner.setText(balancePartial(this.rawBuffer));
|
|
35
|
+
}
|
|
36
|
+
/** Replace the entire buffer (e.g. when retrying). */
|
|
37
|
+
setText(text) {
|
|
38
|
+
this.rawBuffer = text;
|
|
39
|
+
if (this.finalized) {
|
|
40
|
+
this.inner.setText(text);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
this.inner.setText(balancePartial(text));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Mark the stream complete and swap to the canonical full markdown render.
|
|
48
|
+
* Subsequent appends (rare) keep the canonical render path.
|
|
49
|
+
*/
|
|
50
|
+
finalize(canonical) {
|
|
51
|
+
this.finalized = true;
|
|
52
|
+
if (canonical !== undefined) {
|
|
53
|
+
this.rawBuffer = canonical;
|
|
54
|
+
}
|
|
55
|
+
this.inner.setText(this.rawBuffer);
|
|
56
|
+
}
|
|
57
|
+
getRawText() {
|
|
58
|
+
return this.rawBuffer;
|
|
59
|
+
}
|
|
60
|
+
invalidate() {
|
|
61
|
+
this.inner.invalidate();
|
|
62
|
+
}
|
|
63
|
+
render(width) {
|
|
64
|
+
return this.inner.render(width);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Balance partial markdown so the in-flight render doesn't trip over
|
|
69
|
+
* unfinished syntax. Conservative: no semantic re-parsing, just close the
|
|
70
|
+
* obvious cases (fences, runs of ** / * / ~~, dangling [ ).
|
|
71
|
+
*/
|
|
72
|
+
export function balancePartial(text) {
|
|
73
|
+
let working = text;
|
|
74
|
+
// Trim a trailing dangling link prefix like "see [the docs" or "see [docs](".
|
|
75
|
+
// We only strip the unbalanced opening so the partial render shows the
|
|
76
|
+
// preceding prose without an angry render of brackets.
|
|
77
|
+
const lastOpenBracket = working.lastIndexOf("[");
|
|
78
|
+
if (lastOpenBracket !== -1) {
|
|
79
|
+
const tail = working.slice(lastOpenBracket);
|
|
80
|
+
const closingBracket = tail.indexOf("]");
|
|
81
|
+
const openParen = closingBracket !== -1 ? tail.indexOf("(", closingBracket) : -1;
|
|
82
|
+
const closingParen = openParen !== -1 ? tail.indexOf(")", openParen) : -1;
|
|
83
|
+
if (closingBracket === -1 || (openParen !== -1 && closingParen === -1)) {
|
|
84
|
+
working = working.slice(0, lastOpenBracket);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// Close an open fenced code block. We count fence lines (``` at line start,
|
|
88
|
+
// possibly with lang). Odd count → still inside a fence → append a closing
|
|
89
|
+
// fence so the render terminates cleanly.
|
|
90
|
+
const fenceLines = working.split("\n").filter((line) => /^\s{0,3}```/.test(line));
|
|
91
|
+
if (fenceLines.length % 2 === 1) {
|
|
92
|
+
// If the last fence opener is mid-line (no trailing newline), add one.
|
|
93
|
+
if (!working.endsWith("\n"))
|
|
94
|
+
working += "\n";
|
|
95
|
+
working += "```\n";
|
|
96
|
+
}
|
|
97
|
+
// Strip trailing odd-count strong markers ** that haven't been closed.
|
|
98
|
+
if (countOutsideCode(working, "**") % 2 === 1) {
|
|
99
|
+
working = stripTrailingMarker(working, "**");
|
|
100
|
+
}
|
|
101
|
+
// Same for ~~ (strikethrough).
|
|
102
|
+
if (countOutsideCode(working, "~~") % 2 === 1) {
|
|
103
|
+
working = stripTrailingMarker(working, "~~");
|
|
104
|
+
}
|
|
105
|
+
// Single * is ambiguous (lists, italics); leave it alone.
|
|
106
|
+
return working;
|
|
107
|
+
}
|
|
108
|
+
function countOutsideCode(text, marker) {
|
|
109
|
+
// Naive: count occurrences of `marker` outside backtick spans. Good enough
|
|
110
|
+
// for the partial-balance use case — false positives only mean we leave a
|
|
111
|
+
// run open, which the renderer already tolerates.
|
|
112
|
+
let count = 0;
|
|
113
|
+
let inCode = false;
|
|
114
|
+
let i = 0;
|
|
115
|
+
while (i < text.length) {
|
|
116
|
+
const ch = text[i];
|
|
117
|
+
if (ch === "`") {
|
|
118
|
+
inCode = !inCode;
|
|
119
|
+
i += 1;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (!inCode && text.startsWith(marker, i)) {
|
|
123
|
+
count += 1;
|
|
124
|
+
i += marker.length;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
i += 1;
|
|
128
|
+
}
|
|
129
|
+
return count;
|
|
130
|
+
}
|
|
131
|
+
function stripTrailingMarker(text, marker) {
|
|
132
|
+
const idx = text.lastIndexOf(marker);
|
|
133
|
+
if (idx === -1)
|
|
134
|
+
return text;
|
|
135
|
+
return text.slice(0, idx);
|
|
136
|
+
}
|
|
137
|
+
//# sourceMappingURL=streaming-markdown.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"streaming-markdown.js","sourceRoot":"","sources":["../../src/components/streaming-markdown.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Component } from \"../tui.js\";\nimport { type DefaultTextStyle, Markdown, type MarkdownTheme } from \"./markdown.js\";\n\n/**\n * Markdown renderer for chunked, in-flight assistant text.\n *\n * Wraps Markdown so partial content renders cleanly even while half-formed\n * tokens (open fences, unfinished bold, dangling link brackets) are still\n * arriving. Once the stream finalizes, swap in the canonical render via\n * `finalize()`.\n */\nexport class StreamingMarkdown implements Component {\n\tprivate inner: Markdown;\n\tprivate rawBuffer = \"\";\n\tprivate finalized = false;\n\n\tconstructor(\n\t\tinitial: string,\n\t\treadonly paddingX: number,\n\t\treadonly paddingY: number,\n\t\treadonly theme: MarkdownTheme,\n\t\treadonly defaultTextStyle?: DefaultTextStyle,\n\t) {\n\t\tthis.inner = new Markdown(balancePartial(initial), paddingX, paddingY, theme, defaultTextStyle);\n\t\tthis.rawBuffer = initial;\n\t}\n\n\t/** Append a streamed chunk and update the render. */\n\tappend(chunk: string): void {\n\t\tif (this.finalized) {\n\t\t\tthis.rawBuffer += chunk;\n\t\t\tthis.inner.setText(this.rawBuffer);\n\t\t\treturn;\n\t\t}\n\t\tthis.rawBuffer += chunk;\n\t\tthis.inner.setText(balancePartial(this.rawBuffer));\n\t}\n\n\t/** Replace the entire buffer (e.g. when retrying). */\n\tsetText(text: string): void {\n\t\tthis.rawBuffer = text;\n\t\tif (this.finalized) {\n\t\t\tthis.inner.setText(text);\n\t\t} else {\n\t\t\tthis.inner.setText(balancePartial(text));\n\t\t}\n\t}\n\n\t/**\n\t * Mark the stream complete and swap to the canonical full markdown render.\n\t * Subsequent appends (rare) keep the canonical render path.\n\t */\n\tfinalize(canonical?: string): void {\n\t\tthis.finalized = true;\n\t\tif (canonical !== undefined) {\n\t\t\tthis.rawBuffer = canonical;\n\t\t}\n\t\tthis.inner.setText(this.rawBuffer);\n\t}\n\n\tgetRawText(): string {\n\t\treturn this.rawBuffer;\n\t}\n\n\tinvalidate(): void {\n\t\tthis.inner.invalidate();\n\t}\n\n\trender(width: number): string[] {\n\t\treturn this.inner.render(width);\n\t}\n}\n\n/**\n * Balance partial markdown so the in-flight render doesn't trip over\n * unfinished syntax. Conservative: no semantic re-parsing, just close the\n * obvious cases (fences, runs of ** / * / ~~, dangling [ ).\n */\nexport function balancePartial(text: string): string {\n\tlet working = text;\n\n\t// Trim a trailing dangling link prefix like \"see [the docs\" or \"see [docs](\".\n\t// We only strip the unbalanced opening so the partial render shows the\n\t// preceding prose without an angry render of brackets.\n\tconst lastOpenBracket = working.lastIndexOf(\"[\");\n\tif (lastOpenBracket !== -1) {\n\t\tconst tail = working.slice(lastOpenBracket);\n\t\tconst closingBracket = tail.indexOf(\"]\");\n\t\tconst openParen = closingBracket !== -1 ? tail.indexOf(\"(\", closingBracket) : -1;\n\t\tconst closingParen = openParen !== -1 ? tail.indexOf(\")\", openParen) : -1;\n\t\tif (closingBracket === -1 || (openParen !== -1 && closingParen === -1)) {\n\t\t\tworking = working.slice(0, lastOpenBracket);\n\t\t}\n\t}\n\n\t// Close an open fenced code block. We count fence lines (``` at line start,\n\t// possibly with lang). Odd count → still inside a fence → append a closing\n\t// fence so the render terminates cleanly.\n\tconst fenceLines = working.split(\"\\n\").filter((line) => /^\\s{0,3}```/.test(line));\n\tif (fenceLines.length % 2 === 1) {\n\t\t// If the last fence opener is mid-line (no trailing newline), add one.\n\t\tif (!working.endsWith(\"\\n\")) working += \"\\n\";\n\t\tworking += \"```\\n\";\n\t}\n\n\t// Strip trailing odd-count strong markers ** that haven't been closed.\n\tif (countOutsideCode(working, \"**\") % 2 === 1) {\n\t\tworking = stripTrailingMarker(working, \"**\");\n\t}\n\t// Same for ~~ (strikethrough).\n\tif (countOutsideCode(working, \"~~\") % 2 === 1) {\n\t\tworking = stripTrailingMarker(working, \"~~\");\n\t}\n\t// Single * is ambiguous (lists, italics); leave it alone.\n\n\treturn working;\n}\n\nfunction countOutsideCode(text: string, marker: string): number {\n\t// Naive: count occurrences of `marker` outside backtick spans. Good enough\n\t// for the partial-balance use case — false positives only mean we leave a\n\t// run open, which the renderer already tolerates.\n\tlet count = 0;\n\tlet inCode = false;\n\tlet i = 0;\n\twhile (i < text.length) {\n\t\tconst ch = text[i];\n\t\tif (ch === \"`\") {\n\t\t\tinCode = !inCode;\n\t\t\ti += 1;\n\t\t\tcontinue;\n\t\t}\n\t\tif (!inCode && text.startsWith(marker, i)) {\n\t\t\tcount += 1;\n\t\t\ti += marker.length;\n\t\t\tcontinue;\n\t\t}\n\t\ti += 1;\n\t}\n\treturn count;\n}\n\nfunction stripTrailingMarker(text: string, marker: string): string {\n\tconst idx = text.lastIndexOf(marker);\n\tif (idx === -1) return text;\n\treturn text.slice(0, idx);\n}\n"]}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Component } from "../tui.js";
|
|
2
|
+
/**
|
|
3
|
+
* Text component - displays multi-line text with word wrapping
|
|
4
|
+
*/
|
|
5
|
+
export declare class Text implements Component {
|
|
6
|
+
private text;
|
|
7
|
+
private paddingX;
|
|
8
|
+
private paddingY;
|
|
9
|
+
private customBgFn?;
|
|
10
|
+
private cachedText?;
|
|
11
|
+
private cachedWidth?;
|
|
12
|
+
private cachedLines?;
|
|
13
|
+
constructor(text?: string, paddingX?: number, paddingY?: number, customBgFn?: (text: string) => string);
|
|
14
|
+
setText(text: string): void;
|
|
15
|
+
setCustomBgFn(customBgFn?: (text: string) => string): void;
|
|
16
|
+
invalidate(): void;
|
|
17
|
+
render(width: number): string[];
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=text.d.ts.map
|