@oh-my-pi/pi-tui 3.15.0 → 3.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/components/editor.ts +271 -79
- package/src/components/settings-list.ts +5 -2
- package/src/index.ts +18 -1
- package/src/keybindings.ts +143 -0
- package/src/keys.ts +626 -402
- package/src/terminal.ts +8 -0
- package/src/tui.ts +69 -0
package/src/terminal.ts
CHANGED
|
@@ -51,6 +51,9 @@ export interface Terminal {
|
|
|
51
51
|
clearLine(): void; // Clear current line
|
|
52
52
|
clearFromCursor(): void; // Clear from cursor to end of screen
|
|
53
53
|
clearScreen(): void; // Clear entire screen and move cursor to (0,0)
|
|
54
|
+
|
|
55
|
+
// Title operations
|
|
56
|
+
setTitle(title: string): void; // Set terminal window title
|
|
54
57
|
}
|
|
55
58
|
|
|
56
59
|
/**
|
|
@@ -160,4 +163,9 @@ export class ProcessTerminal implements Terminal {
|
|
|
160
163
|
clearScreen(): void {
|
|
161
164
|
process.stdout.write("\x1b[2J\x1b[H"); // Clear screen and move to home (1,1)
|
|
162
165
|
}
|
|
166
|
+
|
|
167
|
+
setTitle(title: string): void {
|
|
168
|
+
// OSC 0;title BEL - set terminal window title
|
|
169
|
+
process.stdout.write(`\x1b]0;${title}\x07`);
|
|
170
|
+
}
|
|
163
171
|
}
|
package/src/tui.ts
CHANGED
|
@@ -26,6 +26,11 @@ export interface Component {
|
|
|
26
26
|
*/
|
|
27
27
|
handleInput?(data: string): void;
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Optional cursor position within the rendered output (0-based row/col).
|
|
31
|
+
*/
|
|
32
|
+
getCursorPosition?(width: number): { row: number; col: number } | null;
|
|
33
|
+
|
|
29
34
|
/**
|
|
30
35
|
* Invalidate any cached rendering state.
|
|
31
36
|
* Called when theme changes or when component needs to re-render from scratch.
|
|
@@ -62,6 +67,19 @@ export class Container implements Component {
|
|
|
62
67
|
}
|
|
63
68
|
}
|
|
64
69
|
|
|
70
|
+
getCursorPosition(width: number): { row: number; col: number } | null {
|
|
71
|
+
let rowOffset = 0;
|
|
72
|
+
for (const child of this.children) {
|
|
73
|
+
const lines = child.render(width);
|
|
74
|
+
const childCursor = child.getCursorPosition?.(width) ?? null;
|
|
75
|
+
if (childCursor) {
|
|
76
|
+
return { row: rowOffset + childCursor.row, col: childCursor.col };
|
|
77
|
+
}
|
|
78
|
+
rowOffset += lines.length;
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
65
83
|
render(width: number): string[] {
|
|
66
84
|
const lines: string[] = [];
|
|
67
85
|
for (const child of this.children) {
|
|
@@ -84,6 +102,7 @@ export class TUI extends Container {
|
|
|
84
102
|
public onDebug?: () => void;
|
|
85
103
|
private renderRequested = false;
|
|
86
104
|
private cursorRow = 0; // Track where cursor is (0-indexed, relative to our first line)
|
|
105
|
+
private previousCursor: { row: number; col: number } | null = null;
|
|
87
106
|
private inputBuffer = ""; // Buffer for parsing terminal responses
|
|
88
107
|
private cellSizeQueryPending = false;
|
|
89
108
|
private inputQueue: string[] = []; // Queue input during cell size query to avoid interleaving
|
|
@@ -132,6 +151,7 @@ export class TUI extends Container {
|
|
|
132
151
|
this.previousLines = [];
|
|
133
152
|
this.previousWidth = 0;
|
|
134
153
|
this.cursorRow = 0;
|
|
154
|
+
this.previousCursor = null;
|
|
135
155
|
}
|
|
136
156
|
if (this.renderRequested) return;
|
|
137
157
|
this.renderRequested = true;
|
|
@@ -141,6 +161,42 @@ export class TUI extends Container {
|
|
|
141
161
|
});
|
|
142
162
|
}
|
|
143
163
|
|
|
164
|
+
private areCursorsEqual(
|
|
165
|
+
left: { row: number; col: number } | null,
|
|
166
|
+
right: { row: number; col: number } | null,
|
|
167
|
+
): boolean {
|
|
168
|
+
if (!left && !right) return true;
|
|
169
|
+
if (!left || !right) return false;
|
|
170
|
+
return left.row === right.row && left.col === right.col;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private updateHardwareCursor(
|
|
174
|
+
width: number,
|
|
175
|
+
totalLines: number,
|
|
176
|
+
cursor: { row: number; col: number } | null,
|
|
177
|
+
currentCursorRow: number,
|
|
178
|
+
): void {
|
|
179
|
+
if (!cursor || totalLines <= 0) {
|
|
180
|
+
this.terminal.hideCursor();
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const targetRow = Math.max(0, Math.min(cursor.row, totalLines - 1));
|
|
185
|
+
const targetCol = Math.max(0, Math.min(cursor.col, width - 1));
|
|
186
|
+
const rowDelta = targetRow - currentCursorRow;
|
|
187
|
+
|
|
188
|
+
let buffer = "";
|
|
189
|
+
if (rowDelta > 0) {
|
|
190
|
+
buffer += `\x1b[${rowDelta}B`;
|
|
191
|
+
} else if (rowDelta < 0) {
|
|
192
|
+
buffer += `\x1b[${-rowDelta}A`;
|
|
193
|
+
}
|
|
194
|
+
buffer += `\r\x1b[${targetCol + 1}G`;
|
|
195
|
+
this.terminal.write(buffer);
|
|
196
|
+
this.cursorRow = targetRow;
|
|
197
|
+
this.terminal.showCursor();
|
|
198
|
+
}
|
|
199
|
+
|
|
144
200
|
private handleInput(data: string): void {
|
|
145
201
|
// If we're waiting for cell size response, buffer input and parse
|
|
146
202
|
if (this.cellSizeQueryPending) {
|
|
@@ -235,6 +291,7 @@ export class TUI extends Container {
|
|
|
235
291
|
|
|
236
292
|
// Render all components to get new lines
|
|
237
293
|
const newLines = this.render(width);
|
|
294
|
+
const cursorInfo = this.getCursorPosition(width);
|
|
238
295
|
|
|
239
296
|
// Width changed - need full re-render
|
|
240
297
|
const widthChanged = this.previousWidth !== 0 && this.previousWidth !== width;
|
|
@@ -250,6 +307,8 @@ export class TUI extends Container {
|
|
|
250
307
|
this.terminal.write(buffer);
|
|
251
308
|
// After rendering N lines, cursor is at end of last line (line N-1)
|
|
252
309
|
this.cursorRow = newLines.length - 1;
|
|
310
|
+
this.updateHardwareCursor(width, newLines.length, cursorInfo, this.cursorRow);
|
|
311
|
+
this.previousCursor = cursorInfo;
|
|
253
312
|
this.previousLines = newLines;
|
|
254
313
|
this.previousWidth = width;
|
|
255
314
|
return;
|
|
@@ -266,6 +325,8 @@ export class TUI extends Container {
|
|
|
266
325
|
buffer += "\x1b[?2026l"; // End synchronized output
|
|
267
326
|
this.terminal.write(buffer);
|
|
268
327
|
this.cursorRow = newLines.length - 1;
|
|
328
|
+
this.updateHardwareCursor(width, newLines.length, cursorInfo, this.cursorRow);
|
|
329
|
+
this.previousCursor = cursorInfo;
|
|
269
330
|
this.previousLines = newLines;
|
|
270
331
|
this.previousWidth = width;
|
|
271
332
|
return;
|
|
@@ -287,6 +348,10 @@ export class TUI extends Container {
|
|
|
287
348
|
|
|
288
349
|
// No changes
|
|
289
350
|
if (firstChanged === -1) {
|
|
351
|
+
if (!this.areCursorsEqual(cursorInfo, this.previousCursor)) {
|
|
352
|
+
this.updateHardwareCursor(width, newLines.length, cursorInfo, currentCursorRow);
|
|
353
|
+
this.previousCursor = cursorInfo;
|
|
354
|
+
}
|
|
290
355
|
return;
|
|
291
356
|
}
|
|
292
357
|
|
|
@@ -306,6 +371,8 @@ export class TUI extends Container {
|
|
|
306
371
|
buffer += "\x1b[?2026l"; // End synchronized output
|
|
307
372
|
this.terminal.write(buffer);
|
|
308
373
|
this.cursorRow = newLines.length - 1;
|
|
374
|
+
this.updateHardwareCursor(width, newLines.length, cursorInfo, this.cursorRow);
|
|
375
|
+
this.previousCursor = cursorInfo;
|
|
309
376
|
this.previousLines = newLines;
|
|
310
377
|
this.previousWidth = width;
|
|
311
378
|
return;
|
|
@@ -372,6 +439,8 @@ export class TUI extends Container {
|
|
|
372
439
|
|
|
373
440
|
// Cursor is now at end of last line
|
|
374
441
|
this.cursorRow = newLines.length - 1;
|
|
442
|
+
this.updateHardwareCursor(width, newLines.length, cursorInfo, this.cursorRow);
|
|
443
|
+
this.previousCursor = cursorInfo;
|
|
375
444
|
|
|
376
445
|
this.previousLines = newLines;
|
|
377
446
|
this.previousWidth = width;
|