@earendil-works/pi-tui 0.84.4 → 0.85.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.
Files changed (71) hide show
  1. package/README.md +53 -4
  2. package/dist/alt-screen-search.d.ts +21 -1
  3. package/dist/alt-screen-search.d.ts.map +1 -1
  4. package/dist/alt-screen-search.js +202 -42
  5. package/dist/alt-screen-search.js.map +1 -1
  6. package/dist/components/box.d.ts +3 -1
  7. package/dist/components/box.d.ts.map +1 -1
  8. package/dist/components/box.js +29 -0
  9. package/dist/components/box.js.map +1 -1
  10. package/dist/components/editor.d.ts +6 -1
  11. package/dist/components/editor.d.ts.map +1 -1
  12. package/dist/components/editor.js +98 -18
  13. package/dist/components/editor.js.map +1 -1
  14. package/dist/components/input.d.ts +12 -1
  15. package/dist/components/input.d.ts.map +1 -1
  16. package/dist/components/input.js +44 -5
  17. package/dist/components/input.js.map +1 -1
  18. package/dist/components/loader.d.ts +2 -0
  19. package/dist/components/loader.d.ts.map +1 -1
  20. package/dist/components/loader.js +10 -3
  21. package/dist/components/loader.js.map +1 -1
  22. package/dist/components/mouse-region.d.ts +12 -0
  23. package/dist/components/mouse-region.d.ts.map +1 -0
  24. package/dist/components/mouse-region.js +21 -0
  25. package/dist/components/mouse-region.js.map +1 -0
  26. package/dist/components/scroll-view.d.ts +6 -3
  27. package/dist/components/scroll-view.d.ts.map +1 -1
  28. package/dist/components/scroll-view.js +8 -2
  29. package/dist/components/scroll-view.js.map +1 -1
  30. package/dist/components/select-list.d.ts +4 -1
  31. package/dist/components/select-list.d.ts.map +1 -1
  32. package/dist/components/select-list.js +49 -2
  33. package/dist/components/select-list.js.map +1 -1
  34. package/dist/components/settings-list.d.ts +5 -1
  35. package/dist/components/settings-list.d.ts.map +1 -1
  36. package/dist/components/settings-list.js +55 -5
  37. package/dist/components/settings-list.js.map +1 -1
  38. package/dist/index.d.ts +2 -1
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +1 -0
  41. package/dist/index.js.map +1 -1
  42. package/dist/latex.d.ts.map +1 -1
  43. package/dist/latex.js +14 -0
  44. package/dist/latex.js.map +1 -1
  45. package/dist/layout.d.ts +3 -1
  46. package/dist/layout.d.ts.map +1 -1
  47. package/dist/layout.js +32 -9
  48. package/dist/layout.js.map +1 -1
  49. package/dist/terminal-image.d.ts.map +1 -1
  50. package/dist/terminal-image.js +1 -4
  51. package/dist/terminal-image.js.map +1 -1
  52. package/dist/terminal.d.ts +6 -0
  53. package/dist/terminal.d.ts.map +1 -1
  54. package/dist/terminal.js +17 -4
  55. package/dist/terminal.js.map +1 -1
  56. package/dist/tui-alt-screen.d.ts +31 -1
  57. package/dist/tui-alt-screen.d.ts.map +1 -1
  58. package/dist/tui-alt-screen.js +342 -41
  59. package/dist/tui-alt-screen.js.map +1 -1
  60. package/dist/tui-main-screen.d.ts.map +1 -1
  61. package/dist/tui-main-screen.js +6 -5
  62. package/dist/tui-main-screen.js.map +1 -1
  63. package/dist/tui.d.ts +84 -6
  64. package/dist/tui.d.ts.map +1 -1
  65. package/dist/tui.js +123 -9
  66. package/dist/tui.js.map +1 -1
  67. package/dist/utils.d.ts +2 -0
  68. package/dist/utils.d.ts.map +1 -1
  69. package/dist/utils.js +9 -0
  70. package/dist/utils.js.map +1 -1
  71. package/package.json +1 -1
@@ -1,7 +1,7 @@
1
1
  import { getKeybindings } from "../keybindings.js";
2
2
  import { decodePrintableKey, matchesKey } from "../keys.js";
3
3
  import { KillRing } from "../kill-ring.js";
4
- import { CURSOR_MARKER } from "../tui.js";
4
+ import { CURSOR_MARKER, } from "../tui.js";
5
5
  import { UndoStack } from "../undo-stack.js";
6
6
  import { cjkBreakRegex, getGraphemeSegmenter, getWordSegmenter, isWhitespaceChar, sliceByColumn, visibleWidth, } from "../utils.js";
7
7
  import { findWordBackward, findWordForward } from "../word-navigation.js";
@@ -180,6 +180,12 @@ function buildDebouncePattern(triggerCharacters) {
180
180
  }
181
181
  function createScrollBorder(direction, hiddenLineCount, width) {
182
182
  const availableWidth = Math.max(0, width);
183
+ const label = ` ${direction} ${hiddenLineCount} more `;
184
+ const labelWidth = visibleWidth(label);
185
+ if (labelWidth + 2 <= availableWidth) {
186
+ const leftWidth = Math.floor((availableWidth - labelWidth) / 2);
187
+ return "─".repeat(leftWidth) + label + "─".repeat(availableWidth - leftWidth - labelWidth);
188
+ }
183
189
  const indicator = `─── ${direction} ${hiddenLineCount} more `;
184
190
  const remaining = availableWidth - visibleWidth(indicator);
185
191
  if (remaining >= 0)
@@ -199,8 +205,10 @@ export class Editor {
199
205
  tui;
200
206
  theme;
201
207
  paddingX = 0;
202
- // Store last render width for cursor navigation
208
+ // Store last render geometry for cursor navigation and mouse hit-testing.
203
209
  lastWidth = 80;
210
+ renderedVisibleLineCount = 1;
211
+ renderedAutocompleteHeight = 0;
204
212
  // Vertical scrolling support
205
213
  scrollOffset = 0;
206
214
  // Border color (can be changed dynamically)
@@ -370,6 +378,14 @@ export class Editor {
370
378
  invalidate() {
371
379
  // No cached state to invalidate currently
372
380
  }
381
+ renderTopBorder(width, hiddenLineCount) {
382
+ const border = hiddenLineCount > 0 ? createScrollBorder("↑", hiddenLineCount, width) : "─".repeat(width);
383
+ return this.borderColor(border);
384
+ }
385
+ renderBottomBorder(width, hiddenLineCount) {
386
+ const border = hiddenLineCount > 0 ? createScrollBorder("↓", hiddenLineCount, width) : "─".repeat(width);
387
+ return this.borderColor(border);
388
+ }
373
389
  render(width) {
374
390
  const maxPadding = Math.max(0, Math.floor((width - 1) / 2));
375
391
  const paddingX = Math.min(this.paddingX, maxPadding);
@@ -379,7 +395,6 @@ export class Editor {
379
395
  const layoutWidth = Math.max(1, contentWidth - (paddingX ? 0 : 1));
380
396
  // Store for cursor navigation (must match wrapping width)
381
397
  this.lastWidth = layoutWidth;
382
- const horizontal = this.borderColor("─");
383
398
  // Layout the text
384
399
  const layoutLines = this.layoutText(layoutWidth);
385
400
  // Calculate max visible lines: 30% of terminal height, minimum 5 lines
@@ -401,17 +416,12 @@ export class Editor {
401
416
  this.scrollOffset = Math.max(0, Math.min(this.scrollOffset, maxScrollOffset));
402
417
  // Get visible lines slice
403
418
  const visibleLines = layoutLines.slice(this.scrollOffset, this.scrollOffset + maxVisibleLines);
419
+ this.renderedVisibleLineCount = visibleLines.length;
404
420
  const result = [];
405
421
  const leftPadding = " ".repeat(paddingX);
406
422
  const rightPadding = leftPadding;
407
423
  // Render top border (with scroll indicator if scrolled down)
408
- if (this.scrollOffset > 0) {
409
- const border = createScrollBorder("↑", this.scrollOffset, width);
410
- result.push(this.borderColor(border));
411
- }
412
- else {
413
- result.push(horizontal.repeat(width));
414
- }
424
+ result.push(this.renderTopBorder(width, this.scrollOffset));
415
425
  // Render each visible layout line
416
426
  // Emit hardware cursor marker when focused so TUI can position the
417
427
  // hardware cursor for IME candidate-window placement even while
@@ -456,16 +466,12 @@ export class Editor {
456
466
  }
457
467
  // Render bottom border (with scroll indicator if more content below)
458
468
  const linesBelow = layoutLines.length - (this.scrollOffset + visibleLines.length);
459
- if (linesBelow > 0) {
460
- const border = createScrollBorder("↓", linesBelow, width);
461
- result.push(this.borderColor(border));
462
- }
463
- else {
464
- result.push(horizontal.repeat(width));
465
- }
469
+ result.push(this.renderBottomBorder(width, linesBelow));
466
470
  // Add autocomplete list if active
471
+ this.renderedAutocompleteHeight = 0;
467
472
  if (this.autocompleteState && this.autocompleteList) {
468
473
  const autocompleteResult = this.autocompleteList.render(contentWidth);
474
+ this.renderedAutocompleteHeight = autocompleteResult.length;
469
475
  for (const line of autocompleteResult) {
470
476
  const lineWidth = visibleWidth(line);
471
477
  const linePadding = " ".repeat(Math.max(0, contentWidth - lineWidth));
@@ -474,6 +480,67 @@ export class Editor {
474
480
  }
475
481
  return result;
476
482
  }
483
+ handleMouse(event) {
484
+ const autocompleteStartRow = this.renderedVisibleLineCount + 2;
485
+ if (this.autocompleteState &&
486
+ this.autocompleteList &&
487
+ event.y >= autocompleteStartRow &&
488
+ event.y < autocompleteStartRow + this.renderedAutocompleteHeight) {
489
+ const maxPadding = Math.max(0, Math.floor((event.width - 1) / 2));
490
+ const paddingX = Math.min(this.paddingX, maxPadding);
491
+ const contentWidth = Math.max(1, event.width - paddingX * 2);
492
+ const result = this.autocompleteList.handleMouse?.({
493
+ ...event,
494
+ x: event.x - paddingX,
495
+ y: event.y - autocompleteStartRow,
496
+ width: contentWidth,
497
+ height: this.renderedAutocompleteHeight,
498
+ });
499
+ return result ? { ...result, focus: true } : undefined;
500
+ }
501
+ // Leave press/drag/release unhandled so the renderer's screen-level text
502
+ // selection can run over the editor rows (drag to select, release to copy).
503
+ // The renderer synthesizes a click when press and release land on the same
504
+ // cell without movement, which is the gesture that positions the cursor.
505
+ if (event.type !== "click" || event.button !== "left")
506
+ return undefined;
507
+ if (event.y <= 0 || event.y > this.renderedVisibleLineCount)
508
+ return { handled: true, focus: true };
509
+ const visualLines = this.buildVisualLineMap(this.lastWidth);
510
+ const visualLineIndex = this.scrollOffset + event.y - 1;
511
+ const visualLine = visualLines[visualLineIndex];
512
+ if (!visualLine)
513
+ return { handled: true, focus: true };
514
+ const logicalLine = this.state.lines[visualLine.logicalLine] ?? "";
515
+ const chunkEnd = visualLine.startCol + visualLine.length;
516
+ const chunk = logicalLine.slice(visualLine.startCol, chunkEnd);
517
+ const maxPadding = Math.max(0, Math.floor((event.width - 1) / 2));
518
+ const paddingX = Math.min(this.paddingX, maxPadding);
519
+ const targetColumn = Math.max(0, event.x - paddingX);
520
+ let visibleColumn = 0;
521
+ let targetIndex = chunk.length;
522
+ let lastGraphemeIndex = 0;
523
+ for (const grapheme of this.segment(chunk, "grapheme")) {
524
+ const nextColumn = visibleColumn + visibleWidth(grapheme.segment);
525
+ lastGraphemeIndex = grapheme.index;
526
+ if (targetColumn < nextColumn) {
527
+ targetIndex = grapheme.index;
528
+ break;
529
+ }
530
+ visibleColumn = nextColumn;
531
+ }
532
+ const isLastSegment = visualLineIndex === visualLines.length - 1 ||
533
+ visualLines[visualLineIndex + 1]?.logicalLine !== visualLine.logicalLine;
534
+ if (!isLastSegment && targetIndex === chunk.length && chunk.length > 0)
535
+ targetIndex = lastGraphemeIndex;
536
+ this.state.cursorLine = visualLine.logicalLine;
537
+ this.setCursorCol(visualLine.startCol + targetIndex);
538
+ this.lastAction = null;
539
+ this.exitHistoryBrowsing();
540
+ if (this.autocompleteState)
541
+ this.updateAutocomplete();
542
+ return { handled: true, focus: true };
543
+ }
477
544
  handleInput(data) {
478
545
  const kb = getKeybindings();
479
546
  // Handle character jump mode (awaiting next character to jump to)
@@ -1804,7 +1871,20 @@ export class Editor {
1804
1871
  }
1805
1872
  createAutocompleteList(prefix, items) {
1806
1873
  const layout = prefix.startsWith("/") ? SLASH_COMMAND_SELECT_LIST_LAYOUT : undefined;
1807
- return new SelectList(items, this.autocompleteMaxVisible, this.theme.selectList, layout);
1874
+ const list = new SelectList(items, this.autocompleteMaxVisible, this.theme.selectList, layout);
1875
+ list.onSelect = (selected) => {
1876
+ if (!this.autocompleteProvider)
1877
+ return;
1878
+ this.pushUndoSnapshot();
1879
+ this.lastAction = null;
1880
+ const result = this.autocompleteProvider.applyCompletion(this.state.lines, this.state.cursorLine, this.state.cursorCol, selected, this.autocompletePrefix);
1881
+ this.state.lines = result.lines;
1882
+ this.state.cursorLine = result.cursorLine;
1883
+ this.setCursorCol(result.cursorCol);
1884
+ this.cancelAutocomplete();
1885
+ this.onChange?.(this.getText());
1886
+ };
1887
+ return list;
1808
1888
  }
1809
1889
  tryTriggerAutocomplete(explicitTab = false) {
1810
1890
  this.requestAutocomplete({ force: false, explicitTab });