@earendil-works/pi-tui 0.78.0 → 0.79.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -1
- package/dist/components/editor.d.ts.map +1 -1
- package/dist/components/editor.js +15 -4
- package/dist/components/editor.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/terminal.d.ts +4 -7
- package/dist/terminal.d.ts.map +1 -1
- package/dist/terminal.js +38 -77
- package/dist/terminal.js.map +1 -1
- package/dist/tui.d.ts +18 -3
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +172 -26
- package/dist/tui.js.map +1 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +3 -0
- package/dist/utils.js.map +1 -1
- package/package.json +1 -1
package/dist/tui.js
CHANGED
|
@@ -118,6 +118,7 @@ export class TUI extends Container {
|
|
|
118
118
|
// Overlay stack for modal components rendered on top of base content
|
|
119
119
|
focusOrderCounter = 0;
|
|
120
120
|
overlayStack = [];
|
|
121
|
+
overlayFocusRestore = { status: "inactive" };
|
|
121
122
|
constructor(terminal, showHardwareCursor) {
|
|
122
123
|
super();
|
|
123
124
|
this.terminal = terminal;
|
|
@@ -152,16 +153,115 @@ export class TUI extends Container {
|
|
|
152
153
|
this.clearOnShrink = enabled;
|
|
153
154
|
}
|
|
154
155
|
setFocus(component) {
|
|
155
|
-
|
|
156
|
+
this.setFocusInternal({ component, overlayFocusRestore: "clear" });
|
|
157
|
+
}
|
|
158
|
+
setFocusInternal({ component, overlayFocusRestore, }) {
|
|
159
|
+
const previousFocus = this.focusedComponent;
|
|
160
|
+
let nextFocus = component;
|
|
161
|
+
const previousFocusedOverlay = previousFocus
|
|
162
|
+
? this.overlayStack.find((entry) => entry.component === previousFocus && this.isOverlayVisible(entry))
|
|
163
|
+
: undefined;
|
|
164
|
+
const nextFocusIsOverlay = nextFocus ? this.overlayStack.some((entry) => entry.component === nextFocus) : false;
|
|
165
|
+
const restoreState = this.getVisibleOverlayFocusRestore();
|
|
166
|
+
if (nextFocus && !nextFocusIsOverlay) {
|
|
167
|
+
if (restoreState.status === "blocked" && restoreState.blockedBy === previousFocus) {
|
|
168
|
+
if (restoreState.resume.status === "focus-target" || !this.isComponentMounted(restoreState.blockedBy)) {
|
|
169
|
+
nextFocus = this.resolveBlockedOverlayFocusResume(restoreState);
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
this.overlayFocusRestore = {
|
|
173
|
+
status: "blocked",
|
|
174
|
+
overlay: restoreState.overlay,
|
|
175
|
+
blockedBy: nextFocus,
|
|
176
|
+
resume: restoreState.resume,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
else if (previousFocusedOverlay &&
|
|
181
|
+
restoreState.status !== "inactive" &&
|
|
182
|
+
restoreState.overlay === previousFocusedOverlay &&
|
|
183
|
+
!this.isOverlayFocusAncestor(previousFocusedOverlay, nextFocus)) {
|
|
184
|
+
this.overlayFocusRestore = {
|
|
185
|
+
status: "blocked",
|
|
186
|
+
overlay: previousFocusedOverlay,
|
|
187
|
+
blockedBy: nextFocus,
|
|
188
|
+
resume: { status: "restore-overlay" },
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
else if (nextFocus === null) {
|
|
193
|
+
if (restoreState.status === "blocked" && restoreState.blockedBy === previousFocus) {
|
|
194
|
+
nextFocus = this.resolveBlockedOverlayFocusResume(restoreState);
|
|
195
|
+
}
|
|
196
|
+
else if (overlayFocusRestore === "clear") {
|
|
197
|
+
this.clearOverlayFocusRestore();
|
|
198
|
+
}
|
|
199
|
+
}
|
|
156
200
|
if (isFocusable(this.focusedComponent)) {
|
|
157
201
|
this.focusedComponent.focused = false;
|
|
158
202
|
}
|
|
159
|
-
this.focusedComponent =
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
203
|
+
this.focusedComponent = nextFocus;
|
|
204
|
+
if (isFocusable(nextFocus)) {
|
|
205
|
+
nextFocus.focused = true;
|
|
206
|
+
}
|
|
207
|
+
const focusedOverlay = nextFocus
|
|
208
|
+
? this.overlayStack.find((entry) => entry.component === nextFocus && this.isOverlayVisible(entry))
|
|
209
|
+
: undefined;
|
|
210
|
+
if (focusedOverlay) {
|
|
211
|
+
this.overlayFocusRestore = { status: "eligible", overlay: focusedOverlay };
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
clearOverlayFocusRestore() {
|
|
215
|
+
this.overlayFocusRestore = { status: "inactive" };
|
|
216
|
+
}
|
|
217
|
+
clearOverlayFocusRestoreFor(overlay) {
|
|
218
|
+
if (this.overlayFocusRestore.status !== "inactive" && this.overlayFocusRestore.overlay === overlay) {
|
|
219
|
+
this.clearOverlayFocusRestore();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
resolveBlockedOverlayFocusResume(restoreState) {
|
|
223
|
+
if (restoreState.resume.status === "restore-overlay")
|
|
224
|
+
return restoreState.overlay.component;
|
|
225
|
+
this.clearOverlayFocusRestore();
|
|
226
|
+
return restoreState.resume.target;
|
|
227
|
+
}
|
|
228
|
+
getVisibleOverlayFocusRestore() {
|
|
229
|
+
const restoreState = this.overlayFocusRestore;
|
|
230
|
+
if (restoreState.status === "inactive")
|
|
231
|
+
return restoreState;
|
|
232
|
+
if (!this.overlayStack.includes(restoreState.overlay) || !this.isOverlayVisible(restoreState.overlay)) {
|
|
233
|
+
return { status: "inactive" };
|
|
234
|
+
}
|
|
235
|
+
return restoreState;
|
|
236
|
+
}
|
|
237
|
+
isOverlayFocusAncestor(entry, component) {
|
|
238
|
+
const visited = new Set();
|
|
239
|
+
let current = entry.preFocus;
|
|
240
|
+
while (current && !visited.has(current)) {
|
|
241
|
+
visited.add(current);
|
|
242
|
+
if (current === component)
|
|
243
|
+
return true;
|
|
244
|
+
current = this.overlayStack.find((overlay) => overlay.component === current)?.preFocus ?? null;
|
|
245
|
+
}
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
retargetOverlayPreFocus(removed) {
|
|
249
|
+
for (const overlay of this.overlayStack) {
|
|
250
|
+
if (overlay !== removed && overlay.preFocus === removed.component) {
|
|
251
|
+
overlay.preFocus = removed.preFocus;
|
|
252
|
+
}
|
|
163
253
|
}
|
|
164
254
|
}
|
|
255
|
+
isComponentMounted(component) {
|
|
256
|
+
return this.children.some((child) => this.containsComponent(child, component));
|
|
257
|
+
}
|
|
258
|
+
containsComponent(root, target) {
|
|
259
|
+
if (root === target)
|
|
260
|
+
return true;
|
|
261
|
+
if (!(root instanceof Container))
|
|
262
|
+
return false;
|
|
263
|
+
return root.children.some((child) => this.containsComponent(child, target));
|
|
264
|
+
}
|
|
165
265
|
/**
|
|
166
266
|
* Show an overlay component with configurable positioning and sizing.
|
|
167
267
|
* Returns a handle to control the overlay's visibility.
|
|
@@ -169,7 +269,7 @@ export class TUI extends Container {
|
|
|
169
269
|
showOverlay(component, options) {
|
|
170
270
|
const entry = {
|
|
171
271
|
component,
|
|
172
|
-
options,
|
|
272
|
+
...(options === undefined ? {} : { options }),
|
|
173
273
|
preFocus: this.focusedComponent,
|
|
174
274
|
hidden: false,
|
|
175
275
|
focusOrder: ++this.focusOrderCounter,
|
|
@@ -186,6 +286,8 @@ export class TUI extends Container {
|
|
|
186
286
|
hide: () => {
|
|
187
287
|
const index = this.overlayStack.indexOf(entry);
|
|
188
288
|
if (index !== -1) {
|
|
289
|
+
this.clearOverlayFocusRestoreFor(entry);
|
|
290
|
+
this.retargetOverlayPreFocus(entry);
|
|
189
291
|
this.overlayStack.splice(index, 1);
|
|
190
292
|
// Restore focus if this overlay had focus
|
|
191
293
|
if (this.focusedComponent === component) {
|
|
@@ -203,6 +305,7 @@ export class TUI extends Container {
|
|
|
203
305
|
entry.hidden = hidden;
|
|
204
306
|
// Update focus when hiding/showing
|
|
205
307
|
if (hidden) {
|
|
308
|
+
this.clearOverlayFocusRestoreFor(entry);
|
|
206
309
|
// If this overlay had focus, move focus to next visible or preFocus
|
|
207
310
|
if (this.focusedComponent === component) {
|
|
208
311
|
const topVisible = this.getTopmostVisibleOverlay();
|
|
@@ -222,17 +325,39 @@ export class TUI extends Container {
|
|
|
222
325
|
focus: () => {
|
|
223
326
|
if (!this.overlayStack.includes(entry) || !this.isOverlayVisible(entry))
|
|
224
327
|
return;
|
|
225
|
-
if (this.focusedComponent !== component) {
|
|
226
|
-
this.setFocus(component);
|
|
227
|
-
}
|
|
228
328
|
entry.focusOrder = ++this.focusOrderCounter;
|
|
329
|
+
this.setFocus(component);
|
|
229
330
|
this.requestRender();
|
|
230
331
|
},
|
|
231
|
-
unfocus: () => {
|
|
232
|
-
|
|
332
|
+
unfocus: (unfocusOptions) => {
|
|
333
|
+
const isFocused = this.focusedComponent === component;
|
|
334
|
+
const restoreState = this.overlayFocusRestore;
|
|
335
|
+
const hasPendingRestore = restoreState.status !== "inactive" && restoreState.overlay === entry;
|
|
336
|
+
if (!isFocused && !hasPendingRestore)
|
|
337
|
+
return;
|
|
338
|
+
if (restoreState.status === "blocked" &&
|
|
339
|
+
restoreState.overlay === entry &&
|
|
340
|
+
this.focusedComponent === restoreState.blockedBy) {
|
|
341
|
+
if (unfocusOptions) {
|
|
342
|
+
this.overlayFocusRestore = {
|
|
343
|
+
status: "blocked",
|
|
344
|
+
overlay: entry,
|
|
345
|
+
blockedBy: restoreState.blockedBy,
|
|
346
|
+
resume: { status: "focus-target", target: unfocusOptions.target },
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
else {
|
|
350
|
+
this.clearOverlayFocusRestore();
|
|
351
|
+
}
|
|
352
|
+
this.requestRender();
|
|
233
353
|
return;
|
|
234
|
-
|
|
235
|
-
this.
|
|
354
|
+
}
|
|
355
|
+
this.clearOverlayFocusRestoreFor(entry);
|
|
356
|
+
if (isFocused || unfocusOptions) {
|
|
357
|
+
const topVisible = this.getTopmostVisibleOverlay();
|
|
358
|
+
const fallbackTarget = topVisible && topVisible !== entry ? topVisible.component : entry.preFocus;
|
|
359
|
+
this.setFocus(unfocusOptions ? unfocusOptions.target : fallbackTarget);
|
|
360
|
+
}
|
|
236
361
|
this.requestRender();
|
|
237
362
|
},
|
|
238
363
|
isFocused: () => this.focusedComponent === component,
|
|
@@ -240,9 +365,12 @@ export class TUI extends Container {
|
|
|
240
365
|
}
|
|
241
366
|
/** Hide the topmost overlay and restore previous focus. */
|
|
242
367
|
hideOverlay() {
|
|
243
|
-
const overlay = this.overlayStack.
|
|
368
|
+
const overlay = this.overlayStack[this.overlayStack.length - 1];
|
|
244
369
|
if (!overlay)
|
|
245
370
|
return;
|
|
371
|
+
this.clearOverlayFocusRestoreFor(overlay);
|
|
372
|
+
this.retargetOverlayPreFocus(overlay);
|
|
373
|
+
this.overlayStack.pop();
|
|
246
374
|
if (this.focusedComponent === overlay.component) {
|
|
247
375
|
// Find topmost visible overlay, or fall back to preFocus
|
|
248
376
|
const topVisible = this.getTopmostVisibleOverlay();
|
|
@@ -265,16 +393,17 @@ export class TUI extends Container {
|
|
|
265
393
|
}
|
|
266
394
|
return true;
|
|
267
395
|
}
|
|
268
|
-
/** Find the
|
|
396
|
+
/** Find the visual-frontmost visible capturing overlay, if any */
|
|
269
397
|
getTopmostVisibleOverlay() {
|
|
270
|
-
|
|
271
|
-
|
|
398
|
+
let topmost;
|
|
399
|
+
for (const overlay of this.overlayStack) {
|
|
400
|
+
if (overlay.options?.nonCapturing || !this.isOverlayVisible(overlay))
|
|
272
401
|
continue;
|
|
273
|
-
if (
|
|
274
|
-
|
|
402
|
+
if (!topmost || overlay.focusOrder > topmost.focusOrder) {
|
|
403
|
+
topmost = overlay;
|
|
275
404
|
}
|
|
276
405
|
}
|
|
277
|
-
return
|
|
406
|
+
return topmost;
|
|
278
407
|
}
|
|
279
408
|
invalidate() {
|
|
280
409
|
super.invalidate();
|
|
@@ -411,8 +540,23 @@ export class TUI extends Container {
|
|
|
411
540
|
this.setFocus(topVisible.component);
|
|
412
541
|
}
|
|
413
542
|
else {
|
|
414
|
-
|
|
415
|
-
|
|
543
|
+
this.setFocusInternal({ component: focusedOverlay.preFocus, overlayFocusRestore: "preserve" });
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
const focusIsOverlay = this.overlayStack.some((o) => o.component === this.focusedComponent);
|
|
547
|
+
if (!focusIsOverlay) {
|
|
548
|
+
const restoreState = this.getVisibleOverlayFocusRestore();
|
|
549
|
+
if (restoreState.status === "eligible") {
|
|
550
|
+
this.setFocus(restoreState.overlay.component);
|
|
551
|
+
}
|
|
552
|
+
else if (restoreState.status === "blocked" && restoreState.blockedBy !== this.focusedComponent) {
|
|
553
|
+
if (restoreState.resume.status === "restore-overlay") {
|
|
554
|
+
this.setFocus(restoreState.overlay.component);
|
|
555
|
+
}
|
|
556
|
+
else {
|
|
557
|
+
this.clearOverlayFocusRestore();
|
|
558
|
+
this.setFocus(restoreState.resume.target);
|
|
559
|
+
}
|
|
416
560
|
}
|
|
417
561
|
}
|
|
418
562
|
// Pass input to focused component (including Ctrl+C)
|
|
@@ -880,16 +1024,18 @@ export class TUI extends Container {
|
|
|
880
1024
|
fullRender(true);
|
|
881
1025
|
return;
|
|
882
1026
|
}
|
|
883
|
-
|
|
884
|
-
|
|
1027
|
+
const clearStartOffset = newLines.length === 0 ? 0 : 1;
|
|
1028
|
+
if (extraLines > 0 && clearStartOffset > 0) {
|
|
1029
|
+
buffer += `\x1b[${clearStartOffset}B`;
|
|
885
1030
|
}
|
|
886
1031
|
for (let i = 0; i < extraLines; i++) {
|
|
887
1032
|
buffer += "\r\x1b[2K";
|
|
888
1033
|
if (i < extraLines - 1)
|
|
889
1034
|
buffer += "\x1b[1B";
|
|
890
1035
|
}
|
|
891
|
-
|
|
892
|
-
|
|
1036
|
+
const moveBack = Math.max(0, extraLines - 1 + clearStartOffset);
|
|
1037
|
+
if (moveBack > 0) {
|
|
1038
|
+
buffer += `\x1b[${moveBack}A`;
|
|
893
1039
|
}
|
|
894
1040
|
buffer += "\x1b[?2026l";
|
|
895
1041
|
this.terminal.write(buffer);
|