@cplieger/web-terminal-ui 8.2.0 → 8.2.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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/kernel/pane.ts +47 -9
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/web-terminal-ui",
3
- "version": "8.2.0",
3
+ "version": "8.2.1",
4
4
  "type": "module",
5
5
  "description": "Reference touch-first browser UI for @cplieger/web-terminal-engine: a composable kernel + feature modules (tabs, activity monitor, mobile toolbar, context menu, IME, predictive echo, viewport handling) and CSS. One createTerminal() call.",
6
6
  "license": "MPL-2.0",
@@ -36,6 +36,8 @@ import type {
36
36
 
37
37
  const { mapKeyboardEvent, bracketTextForPaste, prepareTextForTerminal } = keyboard;
38
38
 
39
+ type GridSize = ReturnType<TerminalEngine["renderer"]["computeSize"]>;
40
+
39
41
  const DEFAULT_WS_PATH = "/ws";
40
42
  // The family that carries the CELL METRICS, alone, never --font-mono's list:
41
43
  // WebKit settles load() over a list as soon as the FIRST family covering the
@@ -468,7 +470,7 @@ function buildPaneInto(
468
470
  function updateMousePointer(): void {
469
471
  termWrap.classList.toggle("wt-mouse-app", engine.modes.getMouseMode() !== 0);
470
472
  }
471
- function measurableSize(): { cols: number; rows: number } | null {
473
+ function measurableSize(): GridSize | null {
472
474
  // A pane hidden by a closed split is `display: none`: the cell width it would
473
475
  // measure is 0, and the first render after a reveal would lay out on it.
474
476
  if (!fontsLoaded || hidden || viewport.isInTransition()) {
@@ -477,11 +479,25 @@ function buildPaneInto(
477
479
  engine.renderer.updateFontMetrics();
478
480
  return engine.renderer.computeSize();
479
481
  }
482
+ let announced: GridSize | null = null;
483
+ let heldGrow = false;
484
+ function announce(size: GridSize): void {
485
+ announced = size;
486
+ heldGrow = false;
487
+ engine.connection.sendResize();
488
+ }
489
+ function releaseHeldGrow(): void {
490
+ const size = measurableSize();
491
+ if (size !== null) {
492
+ announce(size);
493
+ }
494
+ }
480
495
  function maybeSendFirstResize(): void {
481
- if (!wsOpen || measurableSize() === null) {
496
+ const size = wsOpen ? measurableSize() : null;
497
+ if (size === null) {
482
498
  return;
483
499
  }
484
- engine.connection.sendResize();
500
+ announce(size);
485
501
  }
486
502
 
487
503
  const engine: TerminalEngine = createTerminalEngine({
@@ -496,6 +512,9 @@ function buildPaneInto(
496
512
  },
497
513
  onUserScrollChange: (scrolledUp) => {
498
514
  bus.emit("scroll:state", { scrolledUp });
515
+ if (heldGrow && !scrolledUp) {
516
+ releaseHeldGrow();
517
+ }
499
518
  },
500
519
  callbacks: {
501
520
  computeSize: () => engine.renderer.computeSize(),
@@ -513,6 +532,11 @@ function buildPaneInto(
513
532
  if (fontsLoaded) {
514
533
  markReady();
515
534
  }
535
+ // The engine applies a frame to the bound store before this callback,
536
+ // and the alternate screen has no scrollback to hold a size for.
537
+ if (heldGrow && engine.renderer.boundStore().isAlt()) {
538
+ releaseHeldGrow();
539
+ }
516
540
  bus.emit("wire:screen", msg);
517
541
  } else if (msg.type === "title") {
518
542
  // A shell clears its window title when it redraws its prompt after
@@ -971,11 +995,24 @@ function buildPaneInto(
971
995
  box: root,
972
996
  keyboard: services.keyboardInsets,
973
997
  scroll: engine.scroll,
974
- onSettled() {
998
+ onSettled(wasAtBottom) {
975
999
  // The settle is the one resize a keyboard slide or rotation should cost;
976
1000
  // sendResize deduplicates.
977
- if (measurableSize() !== null) {
978
- engine.connection.sendResize();
1001
+ const size = measurableSize();
1002
+ if (size !== null) {
1003
+ // A program repaints on resize, and that repaint can erase or reflow the
1004
+ // scrollback being read, so a taller box waits until the reader follows.
1005
+ if (
1006
+ !wasAtBottom &&
1007
+ announced !== null &&
1008
+ size.cols === announced.cols &&
1009
+ size.rows > announced.rows &&
1010
+ !engine.renderer.boundStore().isAlt()
1011
+ ) {
1012
+ heldGrow = true;
1013
+ } else {
1014
+ announce(size);
1015
+ }
979
1016
  }
980
1017
  composition.positionCompositionView();
981
1018
  },
@@ -1039,10 +1076,11 @@ function buildPaneInto(
1039
1076
  // The gate opened on fallback metrics and the swap period is infinite, so
1040
1077
  // one corrective announce once the bytes land; sendResize deduplicates.
1041
1078
  void doc.fonts.ready.then(() => {
1042
- if (destroyed || measurableSize() === null) {
1079
+ const size = destroyed ? null : measurableSize();
1080
+ if (size === null) {
1043
1081
  return;
1044
1082
  }
1045
- engine.connection.sendResize();
1083
+ announce(size);
1046
1084
  });
1047
1085
  }, onFontSettled);
1048
1086
  } catch (err) {
@@ -1353,7 +1391,7 @@ function buildPaneInto(
1353
1391
  clearActiveSession,
1354
1392
  announceSize() {
1355
1393
  if (!destroyed && fontsLoaded) {
1356
- engine.connection.sendResize();
1394
+ announce(engine.renderer.computeSize());
1357
1395
  }
1358
1396
  },
1359
1397
  focus: focusTerminal,