@oh-my-pi/pi-tui 3.15.0 → 3.15.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/package.json +1 -1
- package/src/components/editor.ts +39 -1
- package/src/tui.ts +69 -0
package/package.json
CHANGED
package/src/components/editor.ts
CHANGED
|
@@ -65,6 +65,7 @@ export class Editor implements Component {
|
|
|
65
65
|
};
|
|
66
66
|
|
|
67
67
|
private theme: EditorTheme;
|
|
68
|
+
private useTerminalCursor = false;
|
|
68
69
|
|
|
69
70
|
// Store last render width for cursor navigation
|
|
70
71
|
private lastWidth: number = 80;
|
|
@@ -114,6 +115,13 @@ export class Editor implements Component {
|
|
|
114
115
|
this.topBorderContent = content;
|
|
115
116
|
}
|
|
116
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Use the real terminal cursor instead of rendering a cursor glyph.
|
|
120
|
+
*/
|
|
121
|
+
setUseTerminalCursor(useTerminalCursor: boolean): void {
|
|
122
|
+
this.useTerminalCursor = useTerminalCursor;
|
|
123
|
+
}
|
|
124
|
+
|
|
117
125
|
/**
|
|
118
126
|
* Add a prompt to history for up/down arrow navigation.
|
|
119
127
|
* Called after successful submission.
|
|
@@ -224,7 +232,7 @@ export class Editor implements Component {
|
|
|
224
232
|
let displayWidth = visibleWidth(layoutLine.text);
|
|
225
233
|
|
|
226
234
|
// Add cursor if this line has it
|
|
227
|
-
if (layoutLine.hasCursor && layoutLine.cursorPos !== undefined) {
|
|
235
|
+
if (!this.useTerminalCursor && layoutLine.hasCursor && layoutLine.cursorPos !== undefined) {
|
|
228
236
|
const before = displayText.slice(0, layoutLine.cursorPos);
|
|
229
237
|
const after = displayText.slice(layoutLine.cursorPos);
|
|
230
238
|
|
|
@@ -285,6 +293,36 @@ export class Editor implements Component {
|
|
|
285
293
|
return result;
|
|
286
294
|
}
|
|
287
295
|
|
|
296
|
+
getCursorPosition(width: number): { row: number; col: number } | null {
|
|
297
|
+
if (!this.useTerminalCursor) return null;
|
|
298
|
+
|
|
299
|
+
const contentWidth = width - 6;
|
|
300
|
+
if (contentWidth <= 0) return null;
|
|
301
|
+
|
|
302
|
+
const layoutLines = this.layoutText(contentWidth);
|
|
303
|
+
for (let i = 0; i < layoutLines.length; i++) {
|
|
304
|
+
const layoutLine = layoutLines[i];
|
|
305
|
+
if (!layoutLine || !layoutLine.hasCursor || layoutLine.cursorPos === undefined) continue;
|
|
306
|
+
|
|
307
|
+
const lineWidth = visibleWidth(layoutLine.text);
|
|
308
|
+
const isCursorAtLineEnd = layoutLine.cursorPos === layoutLine.text.length;
|
|
309
|
+
|
|
310
|
+
if (isCursorAtLineEnd && lineWidth >= contentWidth && layoutLine.text.length > 0) {
|
|
311
|
+
const graphemes = [...segmenter.segment(layoutLine.text)];
|
|
312
|
+
const lastGrapheme = graphemes[graphemes.length - 1]?.segment || "";
|
|
313
|
+
const lastWidth = visibleWidth(lastGrapheme) || 1;
|
|
314
|
+
const colOffset = 3 + Math.max(0, lineWidth - lastWidth);
|
|
315
|
+
return { row: 1 + i, col: colOffset };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const before = layoutLine.text.slice(0, layoutLine.cursorPos);
|
|
319
|
+
const colOffset = 3 + visibleWidth(before);
|
|
320
|
+
return { row: 1 + i, col: colOffset };
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
|
|
288
326
|
handleInput(data: string): void {
|
|
289
327
|
// Handle bracketed paste mode
|
|
290
328
|
// Start of paste: \x1b[200~
|
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;
|