@cplieger/web-terminal-engine 3.0.6 → 3.0.7

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 (3) hide show
  1. package/jsr.json +1 -1
  2. package/package.json +1 -1
  3. package/src/render.ts +50 -37
package/jsr.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/web-terminal-engine",
3
- "version": "3.0.6",
3
+ "version": "3.0.7",
4
4
  "exports": {
5
5
  ".": "./src/index.ts"
6
6
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/web-terminal-engine",
3
- "version": "3.0.6",
3
+ "version": "3.0.7",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "typecheck": "tsc -p tsconfig.json",
package/src/render.ts CHANGED
@@ -249,7 +249,7 @@ export function init(opts: {
249
249
  }
250
250
  flushDrainedThisPass = 0;
251
251
  renderNoProgressStreak = 0;
252
- startCursorBlink();
252
+ syncCursorBlink();
253
253
  }
254
254
 
255
255
  /**
@@ -739,7 +739,11 @@ function flushRenderInner(): void {
739
739
  cursorCol = win.cursorCol;
740
740
  cursorHidden = win.cursorHidden;
741
741
  cursorStyleVal = win.cursorStyle;
742
- setCursorBlink(win.cursorBlink);
742
+ blinkEnabled = win.cursorBlink;
743
+ // Reconcile the blink interval with the fresh cursor state (blink mode and
744
+ // DECTCM visibility may both have changed this frame); no-op when the
745
+ // resulting mode is unchanged.
746
+ syncCursorBlink();
743
747
 
744
748
  // Alt screen: render the ephemeral grid instead of the absolute buffer.
745
749
  // Returning here skips the dirtyLines queueing below — safe by invariant:
@@ -954,8 +958,20 @@ function stickToBottomIfFollowing(): void {
954
958
 
955
959
  // --- Cursor blink ---
956
960
  const CURSOR_BLINK_MS = 530;
961
+ // While the application hides the cursor (DECTCM — e.g. a full-screen TUI
962
+ // that paints its own cursor cell, like an agent front-end), the fast toggle
963
+ // would only restyle a display:none overlay, so the interval downshifts to
964
+ // this slow re-check: still polling for the cursor's return (a self-healing
965
+ // backstop even if a transition frame were somehow missed — the flush hook is
966
+ // the primary, instant restart), at a fraction of the 530ms blink's wakeups.
967
+ // For agent consumers the hidden-cursor state is the session's steady state.
968
+ const CURSOR_RECHECK_MS = 4000;
957
969
  let blinkInterval: ReturnType<typeof setInterval> | null = null;
958
970
  let blinkEnabled = true;
971
+ // What the running interval (if any) is configured as. syncCursorBlink()
972
+ // reconfigures only on mode change, so it is safe to call on every flush.
973
+ type BlinkMode = "off" | "idle" | "fast";
974
+ let blinkMode: BlinkMode = "off";
959
975
 
960
976
  // hiddenDoc reports whether the page is currently background/hidden. The blink
961
977
  // interval is gated on it: rAF-driven flushes already freeze in a hidden tab,
@@ -965,25 +981,42 @@ function hiddenDoc(): boolean {
965
981
  return typeof document !== "undefined" && document.visibilityState === "hidden";
966
982
  }
967
983
 
968
- function startCursorBlink(): void {
969
- if (blinkInterval !== null || !blinkEnabled || hiddenDoc()) {
970
- return;
984
+ // desiredBlinkMode folds the three gates into an interval mode: no timer at
985
+ // all when blinking is disabled or the page is hidden (both have event-driven
986
+ // restarts — the next mode frame's flush and visibilitychange), the fast
987
+ // phase toggle when the cursor is visible, and the slow re-check while the
988
+ // application hides it.
989
+ function desiredBlinkMode(): BlinkMode {
990
+ if (!blinkEnabled || hiddenDoc()) {
991
+ return "off";
971
992
  }
972
- // The blink class lives on termWrap: the caret overlay is a termWrap child
973
- // (not inside output), and the `.cursor-blink-off .term-cursor` descendant
974
- // selector must reach it.
975
- termWrap.classList.remove("cursor-blink-off");
976
- blinkInterval = setInterval(() => {
977
- termWrap.classList.toggle("cursor-blink-off");
978
- }, CURSOR_BLINK_MS);
993
+ return cursorHidden ? "idle" : "fast";
979
994
  }
980
995
 
981
- function stopCursorBlink(): void {
996
+ // syncCursorBlink reconciles the interval with the current gate state; every
997
+ // mode change resets the phase to solid. Called from the flush (blink mode +
998
+ // DECTCM state), the visibilitychange listener, init(), and the idle re-check
999
+ // itself. The blink class lives on termWrap: the caret overlay is a termWrap
1000
+ // child (not inside output), and the `.cursor-blink-off .term-cursor`
1001
+ // descendant selector must reach it.
1002
+ function syncCursorBlink(): void {
1003
+ const mode = desiredBlinkMode();
1004
+ if (mode === blinkMode) {
1005
+ return;
1006
+ }
1007
+ blinkMode = mode;
982
1008
  if (blinkInterval !== null) {
983
1009
  clearInterval(blinkInterval);
984
1010
  blinkInterval = null;
985
1011
  }
986
1012
  termWrap.classList.remove("cursor-blink-off");
1013
+ if (mode === "fast") {
1014
+ blinkInterval = setInterval(() => {
1015
+ termWrap.classList.toggle("cursor-blink-off");
1016
+ }, CURSOR_BLINK_MS);
1017
+ } else if (mode === "idle") {
1018
+ blinkInterval = setInterval(syncCursorBlink, CURSOR_RECHECK_MS);
1019
+ }
987
1020
  }
988
1021
 
989
1022
  // Pause the interval while hidden; resume (cursor solid, phase reset) when the
@@ -995,33 +1028,13 @@ if (typeof document !== "undefined") {
995
1028
  if ((termWrap as HTMLElement | undefined) === undefined) {
996
1029
  return;
997
1030
  }
998
- if (hiddenDoc()) {
999
- // Stop the timer only: blinkEnabled keeps the server-driven state so
1000
- // foregrounding can restart without waiting for the next mode frame.
1001
- if (blinkInterval !== null) {
1002
- clearInterval(blinkInterval);
1003
- blinkInterval = null;
1004
- }
1005
- termWrap.classList.remove("cursor-blink-off");
1006
- return;
1007
- }
1008
- startCursorBlink();
1031
+ // blinkEnabled/cursorHidden keep their server-driven state across a
1032
+ // background stint, so foregrounding restores the right mode without
1033
+ // waiting for the next frame.
1034
+ syncCursorBlink();
1009
1035
  });
1010
1036
  }
1011
1037
 
1012
- /** Called from the flush when cursorBlink state changes. */
1013
- function setCursorBlink(enabled: boolean): void {
1014
- if (enabled === blinkEnabled) {
1015
- return;
1016
- }
1017
- blinkEnabled = enabled;
1018
- if (enabled) {
1019
- startCursorBlink();
1020
- } else {
1021
- stopCursorBlink();
1022
- }
1023
- }
1024
-
1025
1038
  // --- Font metrics & sizing ---
1026
1039
  /**
1027
1040
  * Re-measure the cell width/height from the rendered DOM. Call after any font