@caupulican/pi-tui 0.81.39 → 0.81.41
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/dist/components/editor.d.ts +8 -0
- package/dist/components/editor.d.ts.map +1 -1
- package/dist/components/editor.js +114 -48
- package/dist/components/editor.js.map +1 -1
- package/dist/components/input.d.ts +11 -0
- package/dist/components/input.d.ts.map +1 -1
- package/dist/components/input.js +69 -19
- package/dist/components/input.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/keys.d.ts.map +1 -1
- package/dist/keys.js +6 -5
- package/dist/keys.js.map +1 -1
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +5 -1
- package/dist/tui.js.map +1 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +22 -5
- package/dist/utils.js.map +1 -1
- package/package.json +1 -1
|
@@ -3,7 +3,7 @@ import { decodePrintableKey, matchesKey } from "../keys.js";
|
|
|
3
3
|
import { KillRing } from "../kill-ring.js";
|
|
4
4
|
import { CURSOR_MARKER } from "../tui.js";
|
|
5
5
|
import { UndoStack } from "../undo-stack.js";
|
|
6
|
-
import { getGraphemeSegmenter, getWordSegmenter, isWhitespaceChar,
|
|
6
|
+
import { getGraphemeSegmenter, getWordSegmenter, isWhitespaceChar, sliceByColumn, visibleWidth } from "../utils.js";
|
|
7
7
|
import { findWordBackward, findWordForward } from "../word-navigation.js";
|
|
8
8
|
import { SelectList } from "./select-list.js";
|
|
9
9
|
const graphemeSegmenter = getGraphemeSegmenter();
|
|
@@ -172,6 +172,16 @@ const SLASH_COMMAND_SELECT_LIST_LAYOUT = {
|
|
|
172
172
|
maxPrimaryColumnWidth: 32,
|
|
173
173
|
};
|
|
174
174
|
const ATTACHMENT_AUTOCOMPLETE_DEBOUNCE_MS = 20;
|
|
175
|
+
function createScrollBorder(direction, hiddenLineCount, width) {
|
|
176
|
+
const availableWidth = Math.max(0, width);
|
|
177
|
+
const indicator = `─── ${direction} ${hiddenLineCount} more `;
|
|
178
|
+
const remaining = availableWidth - visibleWidth(indicator);
|
|
179
|
+
if (remaining >= 0)
|
|
180
|
+
return indicator + "─".repeat(remaining);
|
|
181
|
+
const ellipsis = "...".slice(0, availableWidth);
|
|
182
|
+
const indicatorWidth = availableWidth - visibleWidth(ellipsis);
|
|
183
|
+
return sliceByColumn(indicator, 0, indicatorWidth, true) + ellipsis;
|
|
184
|
+
}
|
|
175
185
|
export class Editor {
|
|
176
186
|
state = {
|
|
177
187
|
lines: [""],
|
|
@@ -230,6 +240,7 @@ export class Editor {
|
|
|
230
240
|
snappedFromCursorCol = null;
|
|
231
241
|
// Undo support
|
|
232
242
|
undoStack = new UndoStack();
|
|
243
|
+
privateContent;
|
|
233
244
|
onSubmit;
|
|
234
245
|
onChange;
|
|
235
246
|
disableSubmit = false;
|
|
@@ -237,6 +248,7 @@ export class Editor {
|
|
|
237
248
|
this.tui = tui;
|
|
238
249
|
this.theme = theme;
|
|
239
250
|
this.borderColor = theme.borderColor;
|
|
251
|
+
this.privateContent = options.privateContent ?? false;
|
|
240
252
|
const paddingX = options.paddingX ?? 0;
|
|
241
253
|
this.paddingX = Number.isFinite(paddingX) ? Math.max(0, Math.floor(paddingX)) : 0;
|
|
242
254
|
const maxVisible = options.autocompleteMaxVisible ?? 5;
|
|
@@ -272,6 +284,8 @@ export class Editor {
|
|
|
272
284
|
}
|
|
273
285
|
setAutocompleteProvider(provider) {
|
|
274
286
|
this.cancelAutocomplete();
|
|
287
|
+
if (this.privateContent)
|
|
288
|
+
return;
|
|
275
289
|
this.autocompleteProvider = provider;
|
|
276
290
|
}
|
|
277
291
|
/**
|
|
@@ -279,6 +293,8 @@ export class Editor {
|
|
|
279
293
|
* Called after successful submission.
|
|
280
294
|
*/
|
|
281
295
|
addToHistory(text) {
|
|
296
|
+
if (this.privateContent)
|
|
297
|
+
return;
|
|
282
298
|
const trimmed = text.trim();
|
|
283
299
|
if (!trimmed)
|
|
284
300
|
return;
|
|
@@ -295,6 +311,8 @@ export class Editor {
|
|
|
295
311
|
setHistory(texts) {
|
|
296
312
|
this.history = [];
|
|
297
313
|
this.historyIndex = -1;
|
|
314
|
+
if (this.privateContent)
|
|
315
|
+
return;
|
|
298
316
|
for (const text of texts) {
|
|
299
317
|
this.addToHistory(text);
|
|
300
318
|
}
|
|
@@ -313,6 +331,8 @@ export class Editor {
|
|
|
313
331
|
return currentVisualLine === visualLines.length - 1;
|
|
314
332
|
}
|
|
315
333
|
navigateHistory(direction) {
|
|
334
|
+
if (this.privateContent)
|
|
335
|
+
return;
|
|
316
336
|
this.lastAction = null;
|
|
317
337
|
if (this.history.length === 0)
|
|
318
338
|
return;
|
|
@@ -383,14 +403,7 @@ export class Editor {
|
|
|
383
403
|
const rightPadding = leftPadding;
|
|
384
404
|
// Render top border (with scroll indicator if scrolled down)
|
|
385
405
|
if (this.scrollOffset > 0) {
|
|
386
|
-
|
|
387
|
-
const remaining = width - visibleWidth(indicator);
|
|
388
|
-
if (remaining >= 0) {
|
|
389
|
-
result.push(this.borderColor(indicator + "─".repeat(remaining)));
|
|
390
|
-
}
|
|
391
|
-
else {
|
|
392
|
-
result.push(this.borderColor(truncateToWidth(indicator, width)));
|
|
393
|
-
}
|
|
406
|
+
result.push(this.borderColor(createScrollBorder("↑", this.scrollOffset, width)));
|
|
394
407
|
}
|
|
395
408
|
else {
|
|
396
409
|
result.push(horizontal.repeat(width));
|
|
@@ -438,9 +451,7 @@ export class Editor {
|
|
|
438
451
|
// Render bottom border (with scroll indicator if more content below)
|
|
439
452
|
const linesBelow = layoutLines.length - (this.scrollOffset + visibleLines.length);
|
|
440
453
|
if (linesBelow > 0) {
|
|
441
|
-
|
|
442
|
-
const remaining = width - visibleWidth(indicator);
|
|
443
|
-
result.push(this.borderColor(indicator + "─".repeat(Math.max(0, remaining))));
|
|
454
|
+
result.push(this.borderColor(createScrollBorder("↓", linesBelow, width)));
|
|
444
455
|
}
|
|
445
456
|
else {
|
|
446
457
|
result.push(horizontal.repeat(width));
|
|
@@ -506,7 +517,8 @@ export class Editor {
|
|
|
506
517
|
}
|
|
507
518
|
// Undo
|
|
508
519
|
if (kb.matches(data, "tui.editor.undo")) {
|
|
509
|
-
this.
|
|
520
|
+
if (!this.privateContent)
|
|
521
|
+
this.undo();
|
|
510
522
|
return;
|
|
511
523
|
}
|
|
512
524
|
// Handle autocomplete mode
|
|
@@ -588,11 +600,13 @@ export class Editor {
|
|
|
588
600
|
}
|
|
589
601
|
// Kill ring actions
|
|
590
602
|
if (kb.matches(data, "tui.editor.yank")) {
|
|
591
|
-
this.
|
|
603
|
+
if (!this.privateContent)
|
|
604
|
+
this.yank();
|
|
592
605
|
return;
|
|
593
606
|
}
|
|
594
607
|
if (kb.matches(data, "tui.editor.yankPop")) {
|
|
595
|
-
this.
|
|
608
|
+
if (!this.privateContent)
|
|
609
|
+
this.yankPop();
|
|
596
610
|
return;
|
|
597
611
|
}
|
|
598
612
|
// Cursor movement actions
|
|
@@ -832,8 +846,30 @@ export class Editor {
|
|
|
832
846
|
if (this.getText() !== normalized) {
|
|
833
847
|
this.pushUndoSnapshot();
|
|
834
848
|
}
|
|
849
|
+
this.pastes.clear();
|
|
850
|
+
this.pasteCounter = 0;
|
|
835
851
|
this.setTextInternal(normalized);
|
|
836
852
|
}
|
|
853
|
+
/** Drop every live or historical reference retained by this editor instance. */
|
|
854
|
+
clear() {
|
|
855
|
+
this.cancelAutocomplete();
|
|
856
|
+
this.state = { lines: [""], cursorLine: 0, cursorCol: 0 };
|
|
857
|
+
this.pastes.clear();
|
|
858
|
+
this.pasteCounter = 0;
|
|
859
|
+
this.pasteBuffer = "";
|
|
860
|
+
this.isInPaste = false;
|
|
861
|
+
this.history = [];
|
|
862
|
+
this.historyIndex = -1;
|
|
863
|
+
this.killRing = new KillRing();
|
|
864
|
+
this.undoStack.clear();
|
|
865
|
+
this.lastAction = null;
|
|
866
|
+
this.jumpMode = null;
|
|
867
|
+
this.preferredVisualCol = null;
|
|
868
|
+
this.snappedFromCursorCol = null;
|
|
869
|
+
this.scrollOffset = 0;
|
|
870
|
+
if (this.onChange)
|
|
871
|
+
this.onChange("");
|
|
872
|
+
}
|
|
837
873
|
/**
|
|
838
874
|
* Insert text at the current cursor position.
|
|
839
875
|
* Used for programmatic insertion (e.g., clipboard image markers).
|
|
@@ -854,7 +890,8 @@ export class Editor {
|
|
|
854
890
|
* - Expand tabs to 4 spaces
|
|
855
891
|
*/
|
|
856
892
|
normalizeText(text) {
|
|
857
|
-
|
|
893
|
+
const normalizedLines = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
894
|
+
return this.privateContent ? normalizedLines : normalizedLines.replace(/\t/g, " ");
|
|
858
895
|
}
|
|
859
896
|
/**
|
|
860
897
|
* Internal text insertion at cursor. Handles single and multi-line text.
|
|
@@ -974,11 +1011,11 @@ export class Editor {
|
|
|
974
1011
|
// Filter out non-printable characters except newlines
|
|
975
1012
|
let filteredText = cleanText
|
|
976
1013
|
.split("")
|
|
977
|
-
.filter((char) => char === "\n" || char.charCodeAt(0) >= 32)
|
|
1014
|
+
.filter((char) => char === "\n" || (this.privateContent && char === "\t") || char.charCodeAt(0) >= 32)
|
|
978
1015
|
.join("");
|
|
979
1016
|
// If pasting a file path (starts with /, ~, or .) and the character before
|
|
980
1017
|
// the cursor is a word character, prepend a space for better readability
|
|
981
|
-
if (/^[/~.]/.test(filteredText)) {
|
|
1018
|
+
if (!this.privateContent && /^[/~.]/.test(filteredText)) {
|
|
982
1019
|
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
983
1020
|
const charBeforeCursor = this.state.cursorCol > 0 ? currentLine[this.state.cursorCol - 1] : "";
|
|
984
1021
|
if (charBeforeCursor && /\w/.test(charBeforeCursor)) {
|
|
@@ -989,7 +1026,7 @@ export class Editor {
|
|
|
989
1026
|
const pastedLines = filteredText.split("\n");
|
|
990
1027
|
// Check if this is a large paste (> 10 lines or > 1000 characters)
|
|
991
1028
|
const totalChars = filteredText.length;
|
|
992
|
-
if (pastedLines.length > 10 || totalChars > 1000) {
|
|
1029
|
+
if (!this.privateContent && (pastedLines.length > 10 || totalChars > 1000)) {
|
|
993
1030
|
// Store the paste and insert a marker
|
|
994
1031
|
this.pasteCounter++;
|
|
995
1032
|
const pasteId = this.pasteCounter;
|
|
@@ -1041,16 +1078,9 @@ export class Editor {
|
|
|
1041
1078
|
}
|
|
1042
1079
|
submitValue() {
|
|
1043
1080
|
this.cancelAutocomplete();
|
|
1044
|
-
const
|
|
1045
|
-
|
|
1046
|
-
this.
|
|
1047
|
-
this.pasteCounter = 0;
|
|
1048
|
-
this.historyIndex = -1;
|
|
1049
|
-
this.scrollOffset = 0;
|
|
1050
|
-
this.undoStack.clear();
|
|
1051
|
-
this.lastAction = null;
|
|
1052
|
-
if (this.onChange)
|
|
1053
|
-
this.onChange("");
|
|
1081
|
+
const expanded = this.expandPasteMarkers(this.state.lines.join("\n"));
|
|
1082
|
+
const result = this.privateContent ? expanded : expanded.trim();
|
|
1083
|
+
this.clear();
|
|
1054
1084
|
if (this.onSubmit)
|
|
1055
1085
|
this.onSubmit(result);
|
|
1056
1086
|
}
|
|
@@ -1060,12 +1090,28 @@ export class Editor {
|
|
|
1060
1090
|
if (this.state.cursorCol > 0) {
|
|
1061
1091
|
this.pushUndoSnapshot();
|
|
1062
1092
|
// Delete grapheme before cursor (handles emojis, combining characters, etc.)
|
|
1063
|
-
|
|
1093
|
+
let line = this.state.lines[this.state.cursorLine] || "";
|
|
1064
1094
|
const beforeCursor = line.slice(0, this.state.cursorCol);
|
|
1065
1095
|
// Find the last grapheme in the text before cursor
|
|
1066
1096
|
const graphemes = [...this.segment(beforeCursor, "grapheme")];
|
|
1067
1097
|
const lastGrapheme = graphemes[graphemes.length - 1];
|
|
1068
1098
|
const graphemeLength = lastGrapheme ? lastGrapheme.segment.length : 1;
|
|
1099
|
+
const pasteMarker = PASTE_MARKER_SINGLE.exec(lastGrapheme?.segment ?? "");
|
|
1100
|
+
if (pasteMarker) {
|
|
1101
|
+
const targetId = Number(pasteMarker[1]);
|
|
1102
|
+
this.pastes.delete(targetId);
|
|
1103
|
+
this.pasteCounter--;
|
|
1104
|
+
const higherIds = [...this.pastes.keys()].filter((id) => id > targetId).sort((a, b) => a - b);
|
|
1105
|
+
for (const id of higherIds) {
|
|
1106
|
+
this.pastes.set(id - 1, this.pastes.get(id));
|
|
1107
|
+
this.pastes.delete(id);
|
|
1108
|
+
}
|
|
1109
|
+
this.state.lines = this.state.lines.map((currentLine) => currentLine.replace(PASTE_MARKER_REGEX, (fullMatch, idGroup, suffixGroup) => {
|
|
1110
|
+
const id = Number(idGroup);
|
|
1111
|
+
return id <= targetId ? fullMatch : `[paste #${id - 1}${suffixGroup}]`;
|
|
1112
|
+
}));
|
|
1113
|
+
line = this.state.lines[this.state.cursorLine] || "";
|
|
1114
|
+
}
|
|
1069
1115
|
const before = line.slice(0, this.state.cursorCol - graphemeLength);
|
|
1070
1116
|
const after = line.slice(this.state.cursorCol);
|
|
1071
1117
|
this.state.lines[this.state.cursorLine] = before + after;
|
|
@@ -1243,8 +1289,10 @@ export class Editor {
|
|
|
1243
1289
|
this.pushUndoSnapshot();
|
|
1244
1290
|
// Calculate text to be deleted and save to kill ring (backward deletion = prepend)
|
|
1245
1291
|
const deletedText = currentLine.slice(0, this.state.cursorCol);
|
|
1246
|
-
this.
|
|
1247
|
-
|
|
1292
|
+
if (!this.privateContent) {
|
|
1293
|
+
this.killRing.push(deletedText, { prepend: true, accumulate: this.lastAction === "kill" });
|
|
1294
|
+
this.lastAction = "kill";
|
|
1295
|
+
}
|
|
1248
1296
|
// Delete from start of line up to cursor
|
|
1249
1297
|
this.state.lines[this.state.cursorLine] = currentLine.slice(this.state.cursorCol);
|
|
1250
1298
|
this.setCursorCol(0);
|
|
@@ -1252,8 +1300,10 @@ export class Editor {
|
|
|
1252
1300
|
else if (this.state.cursorLine > 0) {
|
|
1253
1301
|
this.pushUndoSnapshot();
|
|
1254
1302
|
// At start of line - merge with previous line, treating newline as deleted text
|
|
1255
|
-
this.
|
|
1256
|
-
|
|
1303
|
+
if (!this.privateContent) {
|
|
1304
|
+
this.killRing.push("\n", { prepend: true, accumulate: this.lastAction === "kill" });
|
|
1305
|
+
this.lastAction = "kill";
|
|
1306
|
+
}
|
|
1257
1307
|
const previousLine = this.state.lines[this.state.cursorLine - 1] || "";
|
|
1258
1308
|
this.state.lines[this.state.cursorLine - 1] = previousLine + currentLine;
|
|
1259
1309
|
this.state.lines.splice(this.state.cursorLine, 1);
|
|
@@ -1271,16 +1321,20 @@ export class Editor {
|
|
|
1271
1321
|
this.pushUndoSnapshot();
|
|
1272
1322
|
// Calculate text to be deleted and save to kill ring (forward deletion = append)
|
|
1273
1323
|
const deletedText = currentLine.slice(this.state.cursorCol);
|
|
1274
|
-
this.
|
|
1275
|
-
|
|
1324
|
+
if (!this.privateContent) {
|
|
1325
|
+
this.killRing.push(deletedText, { prepend: false, accumulate: this.lastAction === "kill" });
|
|
1326
|
+
this.lastAction = "kill";
|
|
1327
|
+
}
|
|
1276
1328
|
// Delete from cursor to end of line
|
|
1277
1329
|
this.state.lines[this.state.cursorLine] = currentLine.slice(0, this.state.cursorCol);
|
|
1278
1330
|
}
|
|
1279
1331
|
else if (this.state.cursorLine < this.state.lines.length - 1) {
|
|
1280
1332
|
this.pushUndoSnapshot();
|
|
1281
1333
|
// At end of line - merge with next line, treating newline as deleted text
|
|
1282
|
-
this.
|
|
1283
|
-
|
|
1334
|
+
if (!this.privateContent) {
|
|
1335
|
+
this.killRing.push("\n", { prepend: false, accumulate: this.lastAction === "kill" });
|
|
1336
|
+
this.lastAction = "kill";
|
|
1337
|
+
}
|
|
1284
1338
|
const nextLine = this.state.lines[this.state.cursorLine + 1] || "";
|
|
1285
1339
|
this.state.lines[this.state.cursorLine] = currentLine + nextLine;
|
|
1286
1340
|
this.state.lines.splice(this.state.cursorLine + 1, 1);
|
|
@@ -1297,8 +1351,10 @@ export class Editor {
|
|
|
1297
1351
|
if (this.state.cursorLine > 0) {
|
|
1298
1352
|
this.pushUndoSnapshot();
|
|
1299
1353
|
// Treat newline as deleted text (backward deletion = prepend)
|
|
1300
|
-
this.
|
|
1301
|
-
|
|
1354
|
+
if (!this.privateContent) {
|
|
1355
|
+
this.killRing.push("\n", { prepend: true, accumulate: this.lastAction === "kill" });
|
|
1356
|
+
this.lastAction = "kill";
|
|
1357
|
+
}
|
|
1302
1358
|
const previousLine = this.state.lines[this.state.cursorLine - 1] || "";
|
|
1303
1359
|
this.state.lines[this.state.cursorLine - 1] = previousLine + currentLine;
|
|
1304
1360
|
this.state.lines.splice(this.state.cursorLine, 1);
|
|
@@ -1315,8 +1371,10 @@ export class Editor {
|
|
|
1315
1371
|
const deleteFrom = this.state.cursorCol;
|
|
1316
1372
|
this.setCursorCol(oldCursorCol);
|
|
1317
1373
|
const deletedText = currentLine.slice(deleteFrom, this.state.cursorCol);
|
|
1318
|
-
this.
|
|
1319
|
-
|
|
1374
|
+
if (!this.privateContent) {
|
|
1375
|
+
this.killRing.push(deletedText, { prepend: true, accumulate: wasKill });
|
|
1376
|
+
this.lastAction = "kill";
|
|
1377
|
+
}
|
|
1320
1378
|
this.state.lines[this.state.cursorLine] =
|
|
1321
1379
|
currentLine.slice(0, deleteFrom) + currentLine.slice(this.state.cursorCol);
|
|
1322
1380
|
this.setCursorCol(deleteFrom);
|
|
@@ -1333,8 +1391,10 @@ export class Editor {
|
|
|
1333
1391
|
if (this.state.cursorLine < this.state.lines.length - 1) {
|
|
1334
1392
|
this.pushUndoSnapshot();
|
|
1335
1393
|
// Treat newline as deleted text (forward deletion = append)
|
|
1336
|
-
this.
|
|
1337
|
-
|
|
1394
|
+
if (!this.privateContent) {
|
|
1395
|
+
this.killRing.push("\n", { prepend: false, accumulate: this.lastAction === "kill" });
|
|
1396
|
+
this.lastAction = "kill";
|
|
1397
|
+
}
|
|
1338
1398
|
const nextLine = this.state.lines[this.state.cursorLine + 1] || "";
|
|
1339
1399
|
this.state.lines[this.state.cursorLine] = currentLine + nextLine;
|
|
1340
1400
|
this.state.lines.splice(this.state.cursorLine + 1, 1);
|
|
@@ -1349,8 +1409,10 @@ export class Editor {
|
|
|
1349
1409
|
const deleteTo = this.state.cursorCol;
|
|
1350
1410
|
this.setCursorCol(oldCursorCol);
|
|
1351
1411
|
const deletedText = currentLine.slice(this.state.cursorCol, deleteTo);
|
|
1352
|
-
this.
|
|
1353
|
-
|
|
1412
|
+
if (!this.privateContent) {
|
|
1413
|
+
this.killRing.push(deletedText, { prepend: false, accumulate: wasKill });
|
|
1414
|
+
this.lastAction = "kill";
|
|
1415
|
+
}
|
|
1354
1416
|
this.state.lines[this.state.cursorLine] =
|
|
1355
1417
|
currentLine.slice(0, this.state.cursorCol) + currentLine.slice(deleteTo);
|
|
1356
1418
|
}
|
|
@@ -1640,14 +1702,18 @@ export class Editor {
|
|
|
1640
1702
|
}
|
|
1641
1703
|
}
|
|
1642
1704
|
pushUndoSnapshot() {
|
|
1643
|
-
|
|
1705
|
+
if (this.privateContent)
|
|
1706
|
+
return;
|
|
1707
|
+
this.undoStack.push({ state: this.state, pastes: this.pastes, pasteCounter: this.pasteCounter });
|
|
1644
1708
|
}
|
|
1645
1709
|
undo() {
|
|
1646
1710
|
this.historyIndex = -1; // Exit history browsing mode
|
|
1647
1711
|
const snapshot = this.undoStack.pop();
|
|
1648
1712
|
if (!snapshot)
|
|
1649
1713
|
return;
|
|
1650
|
-
Object.assign(this.state, snapshot);
|
|
1714
|
+
Object.assign(this.state, snapshot.state);
|
|
1715
|
+
this.pastes = snapshot.pastes;
|
|
1716
|
+
this.pasteCounter = snapshot.pasteCounter;
|
|
1651
1717
|
this.lastAction = null;
|
|
1652
1718
|
this.preferredVisualCol = null;
|
|
1653
1719
|
if (this.onChange) {
|