@f5-sales-demo/pi-tui 21.29.4 → 21.30.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 +3 -3
- package/src/tui.ts +57 -7
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@f5-sales-demo/pi-tui",
|
|
4
|
-
"version": "21.
|
|
4
|
+
"version": "21.30.1",
|
|
5
5
|
"description": "Terminal User Interface library with differential rendering for efficient text-based applications",
|
|
6
6
|
"homepage": "https://github.com/f5-sales-demo/xcsh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"fmt": "biome format --write ."
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@f5-sales-demo/pi-natives": "21.
|
|
41
|
-
"@f5-sales-demo/pi-utils": "21.
|
|
40
|
+
"@f5-sales-demo/pi-natives": "21.30.1",
|
|
41
|
+
"@f5-sales-demo/pi-utils": "21.30.1",
|
|
42
42
|
"marked": "18.0.11"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
package/src/tui.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { type MouseRoutable, routeSgrMouseInput } from "./mouse";
|
|
|
9
9
|
import { setAlternateScreenActive, type Terminal } from "./terminal";
|
|
10
10
|
import { ImageProtocol, setCellDimensions, setTerminalImageProtocol, TERMINAL } from "./terminal-capabilities";
|
|
11
11
|
import { isInsideTerminalMultiplexer } from "./terminal-multiplexer";
|
|
12
|
-
import { extractSegments, sliceByColumn, sliceWithWidth, visibleWidth } from "./utils";
|
|
12
|
+
import { extractSegments, getSegmenter, sliceByColumn, sliceWithWidth, visibleWidth } from "./utils";
|
|
13
13
|
|
|
14
14
|
const SEGMENT_RESET = "\x1b[0m";
|
|
15
15
|
const LINE_FIT_MIN_SOURCE_CODE_UNITS = 4096;
|
|
@@ -143,6 +143,60 @@ export function isFocusable(component: Component | null): component is Component
|
|
|
143
143
|
*/
|
|
144
144
|
export const CURSOR_MARKER = "\x1b_pi:c\x07";
|
|
145
145
|
|
|
146
|
+
function insertCursorMarkerAtColumn(line: string, column: number): string {
|
|
147
|
+
if (column <= 0 || line.length === 0) return CURSOR_MARKER + line;
|
|
148
|
+
|
|
149
|
+
let index = 0;
|
|
150
|
+
let visualColumn = 0;
|
|
151
|
+
while (index < line.length && visualColumn < column) {
|
|
152
|
+
if (line.charCodeAt(index) === 0x1b) {
|
|
153
|
+
const end = ansiSequenceEnd(line, index);
|
|
154
|
+
if (end === -1) break;
|
|
155
|
+
const sequenceWidth = visibleWidth(line.slice(index, end));
|
|
156
|
+
if (visualColumn + sequenceWidth > column) break;
|
|
157
|
+
visualColumn += sequenceWidth;
|
|
158
|
+
index = end;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const escapeIndex = line.indexOf("\x1b", index);
|
|
163
|
+
const textEnd = escapeIndex === -1 ? line.length : escapeIndex;
|
|
164
|
+
const text = line.slice(index, textEnd);
|
|
165
|
+
const textStart = index;
|
|
166
|
+
for (const { segment, index: segmentIndex } of getSegmenter().segment(text)) {
|
|
167
|
+
const segmentStart = textStart + segmentIndex;
|
|
168
|
+
const segmentWidth = visibleWidth(segment);
|
|
169
|
+
if (visualColumn + segmentWidth > column) {
|
|
170
|
+
return line.slice(0, segmentStart) + CURSOR_MARKER + line.slice(segmentStart);
|
|
171
|
+
}
|
|
172
|
+
visualColumn += segmentWidth;
|
|
173
|
+
index = segmentStart + segment.length;
|
|
174
|
+
if (visualColumn === column) break;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return line.slice(0, index) + CURSOR_MARKER + line.slice(index);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function fitLinePreservingCursor(raw: string, width: number): string {
|
|
182
|
+
const markerIndex = raw.indexOf(CURSOR_MARKER);
|
|
183
|
+
const withoutCursor = markerIndex === -1 ? raw : raw.replaceAll(CURSOR_MARKER, "");
|
|
184
|
+
let fitted = lineFitSource(withoutCursor, width);
|
|
185
|
+
const sourceWasFitted = fitted !== withoutCursor;
|
|
186
|
+
const fittedWidth = visibleWidth(fitted);
|
|
187
|
+
const widthWasClamped = fittedWidth > width;
|
|
188
|
+
if (widthWasClamped) fitted = sliceByColumn(fitted, 0, width, true);
|
|
189
|
+
|
|
190
|
+
if (markerIndex === -1) return fitted;
|
|
191
|
+
|
|
192
|
+
const boundedBeforeCursor = lineFitSource(raw.slice(0, markerIndex), width);
|
|
193
|
+
const cursorColumn = visibleWidth(boundedBeforeCursor);
|
|
194
|
+
const maxCursorColumn = Math.max(0, Math.min(width - 1, visibleWidth(fitted)));
|
|
195
|
+
if (!sourceWasFitted && !widthWasClamped && cursorColumn <= maxCursorColumn) return raw;
|
|
196
|
+
|
|
197
|
+
return insertCursorMarkerAtColumn(fitted, Math.min(cursorColumn, maxCursorColumn));
|
|
198
|
+
}
|
|
199
|
+
|
|
146
200
|
export { visibleWidth };
|
|
147
201
|
|
|
148
202
|
/**
|
|
@@ -1191,14 +1245,10 @@ export class TUI extends Container {
|
|
|
1191
1245
|
|
|
1192
1246
|
// Clamp any oversized lines before dirty-checking so the truncated form
|
|
1193
1247
|
// matches what we store in #previousLines, preventing perpetual repaints.
|
|
1194
|
-
//
|
|
1195
|
-
// as visible chars; the marker is extracted later in #extractCursorPosition.
|
|
1248
|
+
// Preserve CURSOR_MARKER's visual column while keeping width work bounded.
|
|
1196
1249
|
for (let i = 0; i < newLines.length; i++) {
|
|
1197
1250
|
if (TERMINAL.isImageLine(newLines[i])) continue;
|
|
1198
|
-
|
|
1199
|
-
let fitted = lineFitSource(newLines[i].replaceAll(CURSOR_MARKER, ""), width);
|
|
1200
|
-
if (visibleWidth(fitted) > width) fitted = sliceByColumn(fitted, 0, width, true);
|
|
1201
|
-
newLines[i] = fitted + (hadMarker ? CURSOR_MARKER : "");
|
|
1251
|
+
newLines[i] = fitLinePreservingCursor(newLines[i], width);
|
|
1202
1252
|
}
|
|
1203
1253
|
|
|
1204
1254
|
// Composite overlays into the rendered lines (before differential compare)
|