@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.
@@ -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, truncateToWidth, visibleWidth } from "../utils.js";
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
- const indicator = `─── ↑ ${this.scrollOffset} more `;
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
- const indicator = `─── ↓ ${linesBelow} more `;
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.undo();
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.yank();
603
+ if (!this.privateContent)
604
+ this.yank();
592
605
  return;
593
606
  }
594
607
  if (kb.matches(data, "tui.editor.yankPop")) {
595
- this.yankPop();
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
- return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\t/g, " ");
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 result = this.expandPasteMarkers(this.state.lines.join("\n")).trim();
1045
- this.state = { lines: [""], cursorLine: 0, cursorCol: 0 };
1046
- this.pastes.clear();
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
- const line = this.state.lines[this.state.cursorLine] || "";
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.killRing.push(deletedText, { prepend: true, accumulate: this.lastAction === "kill" });
1247
- this.lastAction = "kill";
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.killRing.push("\n", { prepend: true, accumulate: this.lastAction === "kill" });
1256
- this.lastAction = "kill";
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.killRing.push(deletedText, { prepend: false, accumulate: this.lastAction === "kill" });
1275
- this.lastAction = "kill";
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.killRing.push("\n", { prepend: false, accumulate: this.lastAction === "kill" });
1283
- this.lastAction = "kill";
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.killRing.push("\n", { prepend: true, accumulate: this.lastAction === "kill" });
1301
- this.lastAction = "kill";
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.killRing.push(deletedText, { prepend: true, accumulate: wasKill });
1319
- this.lastAction = "kill";
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.killRing.push("\n", { prepend: false, accumulate: this.lastAction === "kill" });
1337
- this.lastAction = "kill";
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.killRing.push(deletedText, { prepend: false, accumulate: wasKill });
1353
- this.lastAction = "kill";
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
- this.undoStack.push(this.state);
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) {