@kolisachint/hoocode-tui 0.5.66 → 0.5.68

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 (39) hide show
  1. package/dist/components/editor.d.ts +20 -18
  2. package/dist/components/editor.d.ts.map +1 -1
  3. package/dist/components/editor.js +54 -58
  4. package/dist/components/editor.js.map +1 -1
  5. package/dist/components/frame.d.ts +117 -0
  6. package/dist/components/frame.d.ts.map +1 -0
  7. package/dist/components/frame.js +203 -0
  8. package/dist/components/frame.js.map +1 -0
  9. package/dist/components/select-list.d.ts.map +1 -1
  10. package/dist/components/select-list.js +8 -2
  11. package/dist/components/select-list.js.map +1 -1
  12. package/dist/components/settings-list.d.ts.map +1 -1
  13. package/dist/components/settings-list.js +7 -2
  14. package/dist/components/settings-list.js.map +1 -1
  15. package/dist/index.d.ts +3 -1
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +4 -1
  18. package/dist/index.js.map +1 -1
  19. package/dist/keybindings.d.ts +9 -4
  20. package/dist/keybindings.d.ts.map +1 -1
  21. package/dist/keybindings.js +14 -2
  22. package/dist/keybindings.js.map +1 -1
  23. package/dist/mouse.d.ts +75 -0
  24. package/dist/mouse.d.ts.map +1 -0
  25. package/dist/mouse.js +115 -0
  26. package/dist/mouse.js.map +1 -0
  27. package/dist/terminal.d.ts +30 -0
  28. package/dist/terminal.d.ts.map +1 -1
  29. package/dist/terminal.js +49 -0
  30. package/dist/terminal.js.map +1 -1
  31. package/dist/tui.d.ts +286 -0
  32. package/dist/tui.d.ts.map +1 -1
  33. package/dist/tui.js +642 -1
  34. package/dist/tui.js.map +1 -1
  35. package/dist/undo-stack.d.ts +39 -6
  36. package/dist/undo-stack.d.ts.map +1 -1
  37. package/dist/undo-stack.js +59 -6
  38. package/dist/undo-stack.js.map +1 -1
  39. package/package.json +1 -1
@@ -3,7 +3,8 @@ 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 { getSegmenter, isPunctuationChar, isWhitespaceChar, truncateToWidth, visibleWidth } from "../utils.js";
6
+ import { getSegmenter, isPunctuationChar, isWhitespaceChar, visibleWidth } from "../utils.js";
7
+ import { DEFAULT_FRAME_BORDER_CHARS, renderFrameEdge, } from "./frame.js";
7
8
  import { SelectList } from "./select-list.js";
8
9
  const baseSegmenter = getSegmenter();
9
10
  /** Regex matching paste markers like `[paste #1 +123 lines]` or `[paste #2 1234 chars]`. */
@@ -170,23 +171,7 @@ export function wordWrapLine(line, maxWidth, preSegmented) {
170
171
  }
171
172
  return chunks;
172
173
  }
173
- export const DEFAULT_EDITOR_BORDER_CHARS = {
174
- horizontal: "─",
175
- vertical: "│",
176
- topLeft: "┌",
177
- topRight: "┐",
178
- bottomLeft: "└",
179
- bottomRight: "┘",
180
- };
181
- /** Border cells kept between the label and the top-right corner, so it reads as inset rather than jammed. */
182
- const LABEL_RIGHT_INSET = 2;
183
- /**
184
- * Border cells required to the *left* of the label. Below this the label is
185
- * dropped rather than drawn: a label butting straight up against the scroll
186
- * indicator reads as one run-on string, and a border with no run of border in it
187
- * has stopped looking like a border.
188
- */
189
- const MIN_LABEL_LEAD_IN = 4;
174
+ export const DEFAULT_EDITOR_BORDER_CHARS = DEFAULT_FRAME_BORDER_CHARS;
190
175
  const SLASH_COMMAND_SELECT_LIST_LAYOUT = {
191
176
  minPrimaryColumnWidth: 12,
192
177
  maxPrimaryColumnWidth: 32,
@@ -385,45 +370,23 @@ export class Editor {
385
370
  * @param box - Whether to draw corners
386
371
  */
387
372
  renderBorder(edge, hidden, barWidth, box) {
388
- const chars = this.borderChars;
389
- let indicator = "";
390
- if (hidden > 0) {
391
- const arrow = edge === "top" ? "↑" : "↓";
392
- indicator = `${chars.horizontal.repeat(3)} ${arrow} ${hidden} more `;
393
- }
394
- const indicatorWidth = visibleWidth(indicator);
395
- // The label rides on the top border only, and yields to the scroll
396
- // indicator: the indicator says the text is longer than the box, which the
397
- // reader needs *now*, while the label says which session this is, which
398
- // they can also read in the footer.
399
- const label = edge === "top" ? this.topBorderLabel : undefined;
400
- const leadIn = label ? barWidth - indicatorWidth - visibleWidth(label.plain) - LABEL_RIGHT_INSET : -1;
401
- let bar;
402
- if (label && leadIn >= MIN_LABEL_LEAD_IN) {
403
- bar =
404
- this.borderColor(indicator + chars.horizontal.repeat(leadIn)) +
405
- label.styled +
406
- this.borderColor(chars.horizontal.repeat(LABEL_RIGHT_INSET));
407
- }
408
- else if (indicatorWidth > 0) {
409
- const remaining = barWidth - indicatorWidth;
410
- bar = this.borderColor(remaining >= 0 ? indicator + chars.horizontal.repeat(remaining) : truncateToWidth(indicator, barWidth));
411
- }
412
- else {
413
- bar = this.borderColor(chars.horizontal.repeat(barWidth));
414
- }
415
- if (!box)
416
- return bar;
417
- // Corners must stay aligned, so pad back any width lost to truncation.
418
- bar += this.borderColor(chars.horizontal.repeat(Math.max(0, barWidth - visibleWidth(bar))));
419
- const left = edge === "top" ? chars.topLeft : chars.bottomLeft;
420
- const right = edge === "top" ? chars.topRight : chars.bottomRight;
421
- return this.borderColor(left) + bar + this.borderColor(right);
373
+ return renderFrameEdge({
374
+ edge,
375
+ barWidth,
376
+ box,
377
+ chars: this.borderChars,
378
+ color: this.borderColor,
379
+ label: this.topBorderLabel,
380
+ hidden,
381
+ });
422
382
  }
423
383
  render(width) {
424
384
  // Box mode needs 2 columns for the side borders plus at least 1 content
425
385
  // column; below that it degrades to rule mode rather than overflowing.
426
386
  const box = this.border === "box" && width >= 4;
387
+ // `none` is for an editor nested in a frame that already rules it: the
388
+ // rows are the same, the two horizontals are simply not drawn.
389
+ const bare = this.border === "none";
427
390
  const borderWidth = box ? 1 : 0;
428
391
  const innerWidth = Math.max(1, width - borderWidth * 2);
429
392
  const maxPadding = Math.max(0, Math.floor((innerWidth - 1) / 2));
@@ -466,7 +429,8 @@ export class Editor {
466
429
  const leftPadding = " ".repeat(paddingX);
467
430
  const rightPadding = leftPadding;
468
431
  // Render top border (with scroll indicator if scrolled down)
469
- result.push(this.renderBorder("top", this.scrollOffset, barWidth, box));
432
+ if (!bare)
433
+ result.push(this.renderBorder("top", this.scrollOffset, barWidth, box));
470
434
  // Render each visible layout line
471
435
  // Emit hardware cursor marker only when focused and not showing autocomplete
472
436
  const emitCursorMarker = this.focused && !this.autocompleteState;
@@ -516,7 +480,8 @@ export class Editor {
516
480
  }
517
481
  // Render bottom border (with scroll indicator if more content below)
518
482
  const linesBelow = layoutLines.length - (this.scrollOffset + visibleLines.length);
519
- result.push(this.renderBorder("bottom", linesBelow, barWidth, box));
483
+ if (!bare)
484
+ result.push(this.renderBorder("bottom", linesBelow, barWidth, box));
520
485
  // Add autocomplete list if active
521
486
  if (this.autocompleteState && this.autocompleteList) {
522
487
  const autocompleteResult = this.autocompleteList.render(contentWidth);
@@ -583,6 +548,10 @@ export class Editor {
583
548
  this.undo();
584
549
  return;
585
550
  }
551
+ if (kb.matches(data, "tui.editor.redo")) {
552
+ this.redo();
553
+ return;
554
+ }
586
555
  // Handle autocomplete mode
587
556
  if (this.autocompleteState && this.autocompleteList) {
588
557
  if (kb.matches(data, "tui.select.cancel")) {
@@ -1744,10 +1713,18 @@ export class Editor {
1744
1713
  this.undoStack.push(this.state);
1745
1714
  }
1746
1715
  undo() {
1747
- this.historyIndex = -1; // Exit history browsing mode
1748
- const snapshot = this.undoStack.pop();
1716
+ // `undo` rather than `pop`: the stack needs the state being left in order
1717
+ // to have somewhere to bring you back to.
1718
+ this.applySnapshot(this.undoStack.undo(this.state));
1719
+ }
1720
+ redo() {
1721
+ this.applySnapshot(this.undoStack.redo(this.state));
1722
+ }
1723
+ /** Shared by undo and redo: both replace the state and settle the editor. */
1724
+ applySnapshot(snapshot) {
1749
1725
  if (!snapshot)
1750
1726
  return;
1727
+ this.historyIndex = -1; // Exit history browsing mode
1751
1728
  Object.assign(this.state, snapshot);
1752
1729
  this.lastAction = null;
1753
1730
  this.preferredVisualCol = null;
@@ -1988,7 +1965,7 @@ export class Editor {
1988
1965
  if (bestMatchIndex >= 0) {
1989
1966
  this.autocompleteList.setSelectedIndex(bestMatchIndex);
1990
1967
  }
1991
- this.autocompleteState = state;
1968
+ this.setAutocompleteState(state);
1992
1969
  }
1993
1970
  cancelAutocompleteRequest() {
1994
1971
  this.autocompleteStartToken += 1;
@@ -2000,14 +1977,33 @@ export class Editor {
2000
1977
  this.autocompleteAbort = undefined;
2001
1978
  }
2002
1979
  clearAutocompleteUi() {
2003
- this.autocompleteState = null;
1980
+ this.setAutocompleteState(null);
2004
1981
  this.autocompleteList = undefined;
2005
1982
  this.autocompletePrefix = "";
2006
1983
  }
1984
+ /**
1985
+ * The one place the list's presence changes, so it is the one place that can
1986
+ * announce it.
1987
+ *
1988
+ * Suggestions arrive from an async provider, which is why a caller cannot
1989
+ * just look after handling a keystroke: on the keypress that opens the list
1990
+ * the state is still null, and if that keypress was the last one — `/mod`
1991
+ * then a pause — nothing ever looks again. Anyone lending the list screen
1992
+ * space has to hear about it when it actually happens.
1993
+ */
1994
+ setAutocompleteState(state) {
1995
+ const was = this.autocompleteState !== null;
1996
+ this.autocompleteState = state;
1997
+ const now = state !== null;
1998
+ if (was !== now)
1999
+ this.onAutocompleteVisibilityChange?.(now);
2000
+ }
2007
2001
  cancelAutocomplete() {
2008
2002
  this.cancelAutocompleteRequest();
2009
2003
  this.clearAutocompleteUi();
2010
2004
  }
2005
+ /** Called when the completion list appears or disappears. */
2006
+ onAutocompleteVisibilityChange;
2011
2007
  isShowingAutocomplete() {
2012
2008
  return this.autocompleteState !== null;
2013
2009
  }