restty 0.1.20 → 0.1.22

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.
@@ -9095,6 +9095,37 @@ function isLikelyEmojiCodepoint(cp) {
9095
9095
  return true;
9096
9096
  return false;
9097
9097
  }
9098
+ function isVariationSelectorCodepoint(cp) {
9099
+ if (cp >= 65024 && cp <= 65039)
9100
+ return true;
9101
+ if (cp >= 917760 && cp <= 917999)
9102
+ return true;
9103
+ return false;
9104
+ }
9105
+ function isCombiningMarkCodepoint(cp) {
9106
+ if (cp >= 768 && cp <= 879)
9107
+ return true;
9108
+ if (cp >= 6832 && cp <= 6911)
9109
+ return true;
9110
+ if (cp >= 7616 && cp <= 7679)
9111
+ return true;
9112
+ if (cp >= 8400 && cp <= 8447)
9113
+ return true;
9114
+ if (cp >= 65056 && cp <= 65071)
9115
+ return true;
9116
+ return false;
9117
+ }
9118
+ function isCoverageIgnorableCodepoint(cp) {
9119
+ if (cp === 8204 || cp === 8205)
9120
+ return true;
9121
+ if (isVariationSelectorCodepoint(cp))
9122
+ return true;
9123
+ if (isCombiningMarkCodepoint(cp))
9124
+ return true;
9125
+ if (cp >= 917536 && cp <= 917631)
9126
+ return true;
9127
+ return false;
9128
+ }
9098
9129
  function resolvePresentationPreference(text, chars) {
9099
9130
  if (text.includes("️"))
9100
9131
  return "emoji";
@@ -9109,7 +9140,7 @@ function resolvePresentationPreference(text, chars) {
9109
9140
  }
9110
9141
  return "auto";
9111
9142
  }
9112
- function pickFontIndexForText(state, text, expectedSpan, shapeClusterWithFont) {
9143
+ function pickFontIndexForText(state, text, expectedSpan) {
9113
9144
  if (!state.fonts.length)
9114
9145
  return 0;
9115
9146
  const cacheKey = `${expectedSpan}:${text}`;
@@ -9117,6 +9148,10 @@ function pickFontIndexForText(state, text, expectedSpan, shapeClusterWithFont) {
9117
9148
  if (cached !== undefined)
9118
9149
  return cached;
9119
9150
  const chars = Array.from(text);
9151
+ const requiredChars = chars.filter((ch) => {
9152
+ const cp = ch.codePointAt(0) ?? 0;
9153
+ return !isCoverageIgnorableCodepoint(cp);
9154
+ });
9120
9155
  const firstCp = text.codePointAt(0) ?? 0;
9121
9156
  const nerdSymbol = isNerdSymbolCodepoint(firstCp);
9122
9157
  const presentation = resolvePresentationPreference(text, chars);
@@ -9128,7 +9163,7 @@ function pickFontIndexForText(state, text, expectedSpan, shapeClusterWithFont) {
9128
9163
  if (predicate && !predicate(entry))
9129
9164
  continue;
9130
9165
  let ok = true;
9131
- for (const ch of chars) {
9166
+ for (const ch of requiredChars) {
9132
9167
  if (!fontHasGlyph(entry.font, ch)) {
9133
9168
  ok = false;
9134
9169
  break;
@@ -26688,6 +26723,17 @@ function buildFontAtlasIfNeeded(params) {
26688
26723
  };
26689
26724
  }
26690
26725
 
26726
+ // src/app/clipboard-paste.ts
26727
+ function readPastePayloadFromDataTransfer(dataTransfer) {
26728
+ if (!dataTransfer)
26729
+ return null;
26730
+ const text = dataTransfer.getData("text/plain") || "";
26731
+ return text ? {
26732
+ kind: "text",
26733
+ text
26734
+ } : null;
26735
+ }
26736
+
26691
26737
  // src/app/font-sources.ts
26692
26738
  var DEFAULT_FONT_SOURCES = [
26693
26739
  {
@@ -50280,6 +50326,8 @@ var DEFAULT_EMOJI_CONSTRAINT = {
50280
50326
  };
50281
50327
  function createResttyApp(options) {
50282
50328
  const { canvas: canvasInput, imeInput: imeInputInput, elements, callbacks } = options;
50329
+ const beforeInputHook = options.beforeInput;
50330
+ const beforeRenderOutputHook = options.beforeRenderOutput;
50283
50331
  const session = options.session ?? getDefaultResttyAppSession();
50284
50332
  const textShaper = exports_dist;
50285
50333
  if (!canvasInput) {
@@ -51558,15 +51606,18 @@ function createResttyApp(options) {
51558
51606
  function sendKeyInput(text, source = "key") {
51559
51607
  if (!text)
51560
51608
  return;
51609
+ const intercepted = runBeforeInputHook(text, source);
51610
+ if (!intercepted)
51611
+ return;
51561
51612
  if (source !== "program" && (selectionState.active || selectionState.dragging)) {
51562
51613
  clearSelection();
51563
51614
  }
51564
51615
  if (ptyTransport.isConnected()) {
51565
- const payload = inputHandler.mapKeyForPty(text);
51616
+ const payload = inputHandler.mapKeyForPty(intercepted);
51566
51617
  ptyTransport.sendInput(payload);
51567
51618
  return;
51568
51619
  }
51569
- sendInput(text, source);
51620
+ sendInput(intercepted, source, { skipHooks: true });
51570
51621
  }
51571
51622
  function formatPasteText(text) {
51572
51623
  if (!inputHandler?.isBracketedPaste?.())
@@ -51578,6 +51629,13 @@ function createResttyApp(options) {
51578
51629
  return;
51579
51630
  sendKeyInput(formatPasteText(text));
51580
51631
  }
51632
+ function sendPastePayloadFromDataTransfer(dataTransfer) {
51633
+ const payload = readPastePayloadFromDataTransfer(dataTransfer);
51634
+ if (!payload)
51635
+ return false;
51636
+ sendPasteText(payload.text);
51637
+ return true;
51638
+ }
51581
51639
  function openLink(uri) {
51582
51640
  if (!uri || typeof window === "undefined")
51583
51641
  return;
@@ -51876,13 +51934,16 @@ function createResttyApp(options) {
51876
51934
  if (imeState.composing)
51877
51935
  return;
51878
51936
  if (event.inputType === "insertFromPaste") {
51937
+ event.preventDefault();
51938
+ suppressNextInput = true;
51879
51939
  const pasteText = event.dataTransfer?.getData("text/plain") || event.data || "";
51880
51940
  if (pasteText) {
51881
- event.preventDefault();
51882
- suppressNextInput = true;
51883
51941
  sendPasteText(pasteText);
51884
51942
  imeInput.value = "";
51943
+ return;
51885
51944
  }
51945
+ sendPastePayloadFromDataTransfer(event.dataTransfer);
51946
+ imeInput.value = "";
51886
51947
  return;
51887
51948
  }
51888
51949
  const text = inputHandler.encodeBeforeInput(event);
@@ -51919,13 +51980,16 @@ function createResttyApp(options) {
51919
51980
  const onPaste = (event) => {
51920
51981
  if (!wasmReady || !wasmHandle)
51921
51982
  return;
51983
+ event.preventDefault();
51984
+ suppressNextInput = true;
51922
51985
  const text = event.clipboardData?.getData("text/plain") || "";
51923
51986
  if (text) {
51924
- event.preventDefault();
51925
- suppressNextInput = true;
51926
51987
  sendPasteText(text);
51927
51988
  imeInput.value = "";
51989
+ return;
51928
51990
  }
51991
+ sendPastePayloadFromDataTransfer(event.clipboardData);
51992
+ imeInput.value = "";
51929
51993
  };
51930
51994
  imeInput.addEventListener("compositionstart", onCompositionStart);
51931
51995
  imeInput.addEventListener("compositionupdate", onCompositionUpdate);
@@ -53200,6 +53264,36 @@ function createResttyApp(options) {
53200
53264
  needsRender = true;
53201
53265
  lastRenderTime = 0;
53202
53266
  }
53267
+ function resize(cols, rows) {
53268
+ const nextCols = Math.max(1, Math.floor(Number(cols)));
53269
+ const nextRows = Math.max(1, Math.floor(Number(rows)));
53270
+ if (!Number.isFinite(nextCols) || !Number.isFinite(nextRows))
53271
+ return;
53272
+ const dpr = window.devicePixelRatio || 1;
53273
+ if (dpr !== currentDpr) {
53274
+ currentDpr = dpr;
53275
+ if (dprEl)
53276
+ dprEl.textContent = dpr.toFixed(2);
53277
+ callbacks?.onDpr?.(dpr);
53278
+ }
53279
+ const metrics = computeCellMetrics2();
53280
+ if (!metrics)
53281
+ return;
53282
+ canvas.width = Math.max(1, nextCols * metrics.cellW);
53283
+ canvas.height = Math.max(1, nextRows * metrics.cellH);
53284
+ if (sizeEl)
53285
+ sizeEl.textContent = `${canvas.width}x${canvas.height}`;
53286
+ callbacks?.onCanvasSize?.(canvas.width, canvas.height);
53287
+ resizeState.dpr = currentDpr;
53288
+ resizeState.active = true;
53289
+ resizeState.lastAt = performance.now();
53290
+ resizeState.cols = nextCols;
53291
+ resizeState.rows = nextRows;
53292
+ updateGrid();
53293
+ scheduleTerminalResizeCommit(nextCols, nextRows, { immediate: true });
53294
+ needsRender = true;
53295
+ lastRenderTime = 0;
53296
+ }
53203
53297
  function scheduleSizeUpdate() {
53204
53298
  updateSize();
53205
53299
  if (sizeRaf)
@@ -53209,22 +53303,35 @@ function createResttyApp(options) {
53209
53303
  updateSize();
53210
53304
  });
53211
53305
  }
53306
+ function focusTypingInput() {
53307
+ canvas.focus({ preventScroll: true });
53308
+ if (!imeInput)
53309
+ return;
53310
+ imeInput.focus({ preventScroll: true });
53311
+ if (typeof document !== "undefined" && document.activeElement !== imeInput) {
53312
+ requestAnimationFrame(() => {
53313
+ if (document.activeElement === canvas)
53314
+ imeInput.focus({ preventScroll: true });
53315
+ });
53316
+ }
53317
+ }
53318
+ function focus() {
53319
+ focusTypingInput();
53320
+ isFocused = typeof document !== "undefined" && imeInput ? document.activeElement === canvas || document.activeElement === imeInput : true;
53321
+ }
53322
+ function blur() {
53323
+ if (imeInput && document.activeElement === imeInput) {
53324
+ imeInput.blur();
53325
+ }
53326
+ if (document.activeElement === canvas) {
53327
+ canvas.blur();
53328
+ }
53329
+ isFocused = false;
53330
+ }
53212
53331
  function bindFocusEvents() {
53213
53332
  if (!attachCanvasEvents)
53214
53333
  return;
53215
53334
  canvas.tabIndex = 0;
53216
- const focusTypingInput = () => {
53217
- canvas.focus({ preventScroll: true });
53218
- if (!imeInput)
53219
- return;
53220
- imeInput.focus({ preventScroll: true });
53221
- if (typeof document !== "undefined" && document.activeElement !== imeInput) {
53222
- requestAnimationFrame(() => {
53223
- if (document.activeElement === canvas)
53224
- imeInput.focus({ preventScroll: true });
53225
- });
53226
- }
53227
- };
53228
53335
  const handleFocus = () => {
53229
53336
  isFocused = true;
53230
53337
  focusTypingInput();
@@ -53715,6 +53822,51 @@ function createResttyApp(options) {
53715
53822
  return true;
53716
53823
  return false;
53717
53824
  }
53825
+ function isVariationSelectorCodepoint2(cp) {
53826
+ if (cp >= 65024 && cp <= 65039)
53827
+ return true;
53828
+ if (cp >= 917760 && cp <= 917999)
53829
+ return true;
53830
+ return false;
53831
+ }
53832
+ function isCombiningMarkCodepoint2(cp) {
53833
+ if (cp >= 768 && cp <= 879)
53834
+ return true;
53835
+ if (cp >= 6832 && cp <= 6911)
53836
+ return true;
53837
+ if (cp >= 7616 && cp <= 7679)
53838
+ return true;
53839
+ if (cp >= 8400 && cp <= 8447)
53840
+ return true;
53841
+ if (cp >= 65056 && cp <= 65071)
53842
+ return true;
53843
+ return false;
53844
+ }
53845
+ function isEmojiModifierCodepoint(cp) {
53846
+ return cp >= 127995 && cp <= 127999;
53847
+ }
53848
+ function isCoverageIgnorableCodepoint2(cp) {
53849
+ if (cp === 8204 || cp === 8205)
53850
+ return true;
53851
+ if (isVariationSelectorCodepoint2(cp))
53852
+ return true;
53853
+ if (isCombiningMarkCodepoint2(cp))
53854
+ return true;
53855
+ if (cp >= 917536 && cp <= 917631)
53856
+ return true;
53857
+ return false;
53858
+ }
53859
+ function shouldMergeTrailingClusterCodepoint(cp) {
53860
+ if (cp === 8204 || cp === 8205)
53861
+ return true;
53862
+ if (isVariationSelectorCodepoint2(cp))
53863
+ return true;
53864
+ if (isCombiningMarkCodepoint2(cp))
53865
+ return true;
53866
+ if (isEmojiModifierCodepoint(cp))
53867
+ return true;
53868
+ return false;
53869
+ }
53718
53870
  function resolvePresentationPreference2(text, chars) {
53719
53871
  if (text.includes("️"))
53720
53872
  return "emoji";
@@ -53737,6 +53889,10 @@ function createResttyApp(options) {
53737
53889
  if (cached !== undefined)
53738
53890
  return cached;
53739
53891
  const chars = Array.from(text);
53892
+ const requiredChars = chars.filter((ch) => {
53893
+ const cp = ch.codePointAt(0) ?? 0;
53894
+ return !isCoverageIgnorableCodepoint2(cp);
53895
+ });
53740
53896
  const firstCp = text.codePointAt(0) ?? 0;
53741
53897
  const nerdSymbol = isNerdSymbolCodepoint(firstCp);
53742
53898
  const presentation = resolvePresentationPreference2(text, chars);
@@ -53751,7 +53907,7 @@ function createResttyApp(options) {
53751
53907
  (entry) => hasItalicHint(entry) && !hasBoldHint(entry),
53752
53908
  (entry) => hasItalicHint(entry)
53753
53909
  ] : [];
53754
- const pickFirstMatch = (predicate) => {
53910
+ const pickFirstMatch = (predicate, allowSequenceShapingFallback = false) => {
53755
53911
  for (let i3 = 0;i3 < fontState.fonts.length; i3 += 1) {
53756
53912
  const entry = fontState.fonts[i3];
53757
53913
  if (!entry?.font)
@@ -53759,18 +53915,22 @@ function createResttyApp(options) {
53759
53915
  if (predicate && !predicate(entry))
53760
53916
  continue;
53761
53917
  let ok = true;
53762
- for (const ch of chars) {
53918
+ for (const ch of requiredChars) {
53763
53919
  if (!fontHasGlyph2(entry.font, ch)) {
53764
53920
  ok = false;
53765
53921
  break;
53766
53922
  }
53767
53923
  }
53924
+ if (!ok && allowSequenceShapingFallback) {
53925
+ const shaped = shapeClusterWithFont(entry, text);
53926
+ ok = shaped.glyphs.some((glyph) => (glyph.glyphId ?? 0) !== 0);
53927
+ }
53768
53928
  if (ok)
53769
53929
  return i3;
53770
53930
  }
53771
53931
  return -1;
53772
53932
  };
53773
- const pickWithStyle = (predicate) => {
53933
+ const pickWithStyle = (predicate, allowSequenceShapingFallback = false) => {
53774
53934
  if (styleHintsEnabled) {
53775
53935
  for (let i3 = 0;i3 < stylePredicates.length; i3 += 1) {
53776
53936
  const stylePredicate = stylePredicates[i3];
@@ -53778,12 +53938,12 @@ function createResttyApp(options) {
53778
53938
  if (!stylePredicate(entry))
53779
53939
  return false;
53780
53940
  return predicate ? !!predicate(entry) : true;
53781
- });
53941
+ }, allowSequenceShapingFallback);
53782
53942
  if (styledIndex >= 0)
53783
53943
  return styledIndex;
53784
53944
  }
53785
53945
  }
53786
- return pickFirstMatch(predicate);
53946
+ return pickFirstMatch(predicate, allowSequenceShapingFallback);
53787
53947
  };
53788
53948
  const tryIndex = (index) => {
53789
53949
  if (index < 0)
@@ -53798,7 +53958,7 @@ function createResttyApp(options) {
53798
53958
  return result;
53799
53959
  }
53800
53960
  if (presentation === "emoji") {
53801
- const emojiIndex = pickFirstMatch((entry) => isColorEmojiFont(entry));
53961
+ const emojiIndex = pickFirstMatch((entry) => isColorEmojiFont(entry), true);
53802
53962
  const result = tryIndex(emojiIndex);
53803
53963
  if (result !== null)
53804
53964
  return result;
@@ -54508,10 +54668,13 @@ function createResttyApp(options) {
54508
54668
  }
54509
54669
  let nextSeqIdx = idx + baseSpan;
54510
54670
  let guard = 0;
54511
- while ((text.codePointAt(text.length - 1) ?? 0) === 8205 && nextSeqIdx < rowEnd && guard < 8) {
54671
+ while (nextSeqIdx < rowEnd && guard < 12) {
54512
54672
  const next = readCellCluster(nextSeqIdx);
54513
54673
  if (!next || !next.cp || isSpaceCp(next.cp))
54514
54674
  break;
54675
+ const shouldMerge = text.endsWith("‍") || shouldMergeTrailingClusterCodepoint(next.cp);
54676
+ if (!shouldMerge)
54677
+ break;
54515
54678
  text += next.text;
54516
54679
  baseSpan += next.span;
54517
54680
  mergedEmojiSkip[nextSeqIdx] = 1;
@@ -55374,10 +55537,13 @@ function createResttyApp(options) {
55374
55537
  }
55375
55538
  let nextSeqIdx = idx + baseSpan;
55376
55539
  let guard = 0;
55377
- while ((text.codePointAt(text.length - 1) ?? 0) === 8205 && nextSeqIdx < rowEnd && guard < 8) {
55540
+ while (nextSeqIdx < rowEnd && guard < 12) {
55378
55541
  const next = readCellCluster(nextSeqIdx);
55379
55542
  if (!next || !next.cp || isSpaceCp(next.cp))
55380
55543
  break;
55544
+ const shouldMerge = text.endsWith("‍") || shouldMergeTrailingClusterCodepoint(next.cp);
55545
+ if (!shouldMerge)
55546
+ break;
55381
55547
  text += next.text;
55382
55548
  baseSpan += next.span;
55383
55549
  mergedEmojiSkip[nextSeqIdx] = 1;
@@ -55969,12 +56135,48 @@ function createResttyApp(options) {
55969
56135
  return text.replace(/\r?\n/g, `\r
55970
56136
  `);
55971
56137
  }
55972
- function sendInput(text, source = "program") {
56138
+ function runBeforeInputHook(text, source) {
56139
+ if (!beforeInputHook)
56140
+ return text;
56141
+ try {
56142
+ const next = beforeInputHook({ text, source });
56143
+ if (next === null)
56144
+ return null;
56145
+ if (typeof next === "string")
56146
+ return next;
56147
+ return text;
56148
+ } catch (error) {
56149
+ console.error("[restty] beforeInput hook error:", error);
56150
+ return text;
56151
+ }
56152
+ }
56153
+ function runBeforeRenderOutputHook(text, source) {
56154
+ if (!beforeRenderOutputHook)
56155
+ return text;
56156
+ try {
56157
+ const next = beforeRenderOutputHook({ text, source });
56158
+ if (next === null)
56159
+ return null;
56160
+ if (typeof next === "string")
56161
+ return next;
56162
+ return text;
56163
+ } catch (error) {
56164
+ console.error("[restty] beforeRenderOutput hook error:", error);
56165
+ return text;
56166
+ }
56167
+ }
56168
+ function sendInput(text, source = "program", options2 = {}) {
55973
56169
  if (!wasmReady || !wasm || !wasmHandle)
55974
56170
  return;
55975
56171
  if (!text)
55976
56172
  return;
55977
- const normalized = source === "pty" ? text : normalizeNewlines(text);
56173
+ let intercepted = text;
56174
+ if (!options2.skipHooks) {
56175
+ intercepted = source === "pty" ? runBeforeRenderOutputHook(text, source) : runBeforeInputHook(text, source);
56176
+ }
56177
+ if (!intercepted)
56178
+ return;
56179
+ const normalized = source === "pty" ? intercepted : normalizeNewlines(intercepted);
55978
56180
  if (source === "key") {
55979
56181
  const bytes = textEncoder3.encode(normalized);
55980
56182
  const hex = Array.from(bytes, (b3) => b3.toString(16).padStart(2, "0")).join(" ");
@@ -56040,6 +56242,8 @@ function createResttyApp(options) {
56040
56242
  return false;
56041
56243
  }
56042
56244
  async function pasteFromClipboard() {
56245
+ if (typeof navigator === "undefined" || !navigator.clipboard)
56246
+ return false;
56043
56247
  try {
56044
56248
  const text = await navigator.clipboard.readText();
56045
56249
  if (text) {
@@ -56051,6 +56255,15 @@ function createResttyApp(options) {
56051
56255
  }
56052
56256
  return false;
56053
56257
  }
56258
+ async function handlePasteShortcut(event) {
56259
+ const pasted = await pasteFromClipboard();
56260
+ if (pasted)
56261
+ return;
56262
+ const seq = inputHandler.encodeKeyEvent(event);
56263
+ if (!seq)
56264
+ return;
56265
+ sendKeyInput(seq);
56266
+ }
56054
56267
  function clearScreen() {
56055
56268
  sendInput("\x1B[2J\x1B[H");
56056
56269
  }
@@ -56099,7 +56312,9 @@ function createResttyApp(options) {
56099
56312
  }
56100
56313
  if (wantsPaste) {
56101
56314
  event.preventDefault();
56102
- pasteFromClipboard();
56315
+ if (imeInput)
56316
+ imeInput.focus({ preventScroll: true });
56317
+ handlePasteShortcut(event);
56103
56318
  return;
56104
56319
  }
56105
56320
  const seq = inputHandler.encodeKeyEvent(event);
@@ -56301,6 +56516,9 @@ function createResttyApp(options) {
56301
56516
  copySelectionToClipboard,
56302
56517
  pasteFromClipboard,
56303
56518
  dumpAtlasForCodepoint,
56519
+ resize,
56520
+ focus,
56521
+ blur,
56304
56522
  updateSize,
56305
56523
  getBackend: () => backend
56306
56524
  };
@@ -56420,6 +56638,8 @@ function createResttyAppPaneManager(options) {
56420
56638
  }
56421
56639
 
56422
56640
  // src/app/restty.ts
56641
+ var RESTTY_PLUGIN_API_VERSION = 1;
56642
+
56423
56643
  class ResttyPaneHandle {
56424
56644
  resolvePane;
56425
56645
  constructor(resolvePane) {
@@ -56479,6 +56699,15 @@ class ResttyPaneHandle {
56479
56699
  dumpAtlasForCodepoint(cp) {
56480
56700
  this.resolvePane().app.dumpAtlasForCodepoint(cp);
56481
56701
  }
56702
+ resize(cols, rows) {
56703
+ this.resolvePane().app.resize(cols, rows);
56704
+ }
56705
+ focus() {
56706
+ this.resolvePane().app.focus();
56707
+ }
56708
+ blur() {
56709
+ this.resolvePane().app.blur();
56710
+ }
56482
56711
  updateSize(force) {
56483
56712
  this.resolvePane().app.updateSize(force);
56484
56713
  }
@@ -56493,25 +56722,105 @@ class ResttyPaneHandle {
56493
56722
  class Restty {
56494
56723
  paneManager;
56495
56724
  fontSources;
56725
+ pluginListeners = new Map;
56726
+ pluginRuntimes = new Map;
56727
+ pluginDiagnostics = new Map;
56728
+ inputInterceptors = [];
56729
+ outputInterceptors = [];
56730
+ lifecycleHooks = [];
56731
+ renderHooks = [];
56732
+ nextInterceptorId = 1;
56733
+ nextInterceptorOrder = 1;
56496
56734
  constructor(options) {
56497
- const { createInitialPane = true, appOptions, fontSources, ...paneManagerOptions } = options;
56735
+ const {
56736
+ createInitialPane = true,
56737
+ appOptions,
56738
+ fontSources,
56739
+ onPaneCreated,
56740
+ onPaneClosed,
56741
+ onPaneSplit,
56742
+ onActivePaneChange,
56743
+ onLayoutChanged,
56744
+ ...paneManagerOptions
56745
+ } = options;
56498
56746
  this.fontSources = fontSources ? [...fontSources] : undefined;
56499
56747
  const mergedAppOptions = (context) => {
56500
56748
  const resolved = typeof appOptions === "function" ? appOptions(context) : appOptions ?? {};
56501
- if (!this.fontSources)
56502
- return resolved;
56749
+ const resolvedBeforeInput = resolved.beforeInput;
56750
+ const resolvedBeforeRenderOutput = resolved.beforeRenderOutput;
56503
56751
  return {
56504
56752
  ...resolved,
56505
- fontSources: this.fontSources
56753
+ ...this.fontSources ? { fontSources: this.fontSources } : {},
56754
+ beforeInput: ({ text, source }) => {
56755
+ const maybeUserText = resolvedBeforeInput?.({ text, source });
56756
+ if (maybeUserText === null)
56757
+ return null;
56758
+ const current = maybeUserText === undefined ? text : maybeUserText;
56759
+ return this.applyInputInterceptors(context.id, current, source);
56760
+ },
56761
+ beforeRenderOutput: ({ text, source }) => {
56762
+ this.runRenderHooks({
56763
+ phase: "before",
56764
+ paneId: context.id,
56765
+ text,
56766
+ source,
56767
+ dropped: false
56768
+ });
56769
+ const maybeUserText = resolvedBeforeRenderOutput?.({ text, source });
56770
+ if (maybeUserText === null) {
56771
+ this.runRenderHooks({
56772
+ phase: "after",
56773
+ paneId: context.id,
56774
+ text,
56775
+ source,
56776
+ dropped: true
56777
+ });
56778
+ return null;
56779
+ }
56780
+ const current = maybeUserText === undefined ? text : maybeUserText;
56781
+ const next = this.applyOutputInterceptors(context.id, current, source);
56782
+ this.runRenderHooks({
56783
+ phase: "after",
56784
+ paneId: context.id,
56785
+ text: next === null ? current : next,
56786
+ source,
56787
+ dropped: next === null
56788
+ });
56789
+ return next;
56790
+ }
56506
56791
  };
56507
56792
  };
56508
56793
  this.paneManager = createResttyAppPaneManager({
56509
56794
  ...paneManagerOptions,
56510
- appOptions: mergedAppOptions
56795
+ appOptions: mergedAppOptions,
56796
+ onPaneCreated: (pane) => {
56797
+ this.emitPluginEvent("pane:created", { paneId: pane.id });
56798
+ onPaneCreated?.(pane);
56799
+ },
56800
+ onPaneClosed: (pane) => {
56801
+ this.emitPluginEvent("pane:closed", { paneId: pane.id });
56802
+ onPaneClosed?.(pane);
56803
+ },
56804
+ onPaneSplit: (sourcePane, createdPane, direction) => {
56805
+ this.emitPluginEvent("pane:split", {
56806
+ sourcePaneId: sourcePane.id,
56807
+ createdPaneId: createdPane.id,
56808
+ direction
56809
+ });
56810
+ onPaneSplit?.(sourcePane, createdPane, direction);
56811
+ },
56812
+ onActivePaneChange: (pane) => {
56813
+ this.emitPluginEvent("pane:active-changed", { paneId: pane?.id ?? null });
56814
+ onActivePaneChange?.(pane);
56815
+ },
56816
+ onLayoutChanged: () => {
56817
+ this.emitPluginEvent("layout:changed", {});
56818
+ onLayoutChanged?.();
56819
+ }
56511
56820
  });
56512
56821
  if (createInitialPane) {
56513
56822
  const focus = typeof createInitialPane === "object" ? createInitialPane.focus ?? true : true;
56514
- this.paneManager.createInitialPane({ focus });
56823
+ this.createInitialPane({ focus });
56515
56824
  }
56516
56825
  }
56517
56826
  getPanes() {
@@ -56562,16 +56871,63 @@ class Restty {
56562
56871
  await Promise.all(updates);
56563
56872
  }
56564
56873
  createInitialPane(options) {
56565
- return this.paneManager.createInitialPane(options);
56874
+ this.runLifecycleHooks({ phase: "before", action: "create-initial-pane" });
56875
+ const pane = this.paneManager.createInitialPane(options);
56876
+ this.runLifecycleHooks({
56877
+ phase: "after",
56878
+ action: "create-initial-pane",
56879
+ paneId: pane.id,
56880
+ ok: true
56881
+ });
56882
+ return pane;
56566
56883
  }
56567
56884
  splitActivePane(direction) {
56568
- return this.paneManager.splitActivePane(direction);
56885
+ const sourcePaneId = this.getActivePane()?.id ?? null;
56886
+ this.runLifecycleHooks({
56887
+ phase: "before",
56888
+ action: "split-active-pane",
56889
+ paneId: sourcePaneId,
56890
+ direction
56891
+ });
56892
+ const pane = this.paneManager.splitActivePane(direction);
56893
+ this.runLifecycleHooks({
56894
+ phase: "after",
56895
+ action: "split-active-pane",
56896
+ sourcePaneId: sourcePaneId ?? undefined,
56897
+ createdPaneId: pane?.id ?? null,
56898
+ direction,
56899
+ ok: !!pane
56900
+ });
56901
+ return pane;
56569
56902
  }
56570
56903
  splitPane(id, direction) {
56571
- return this.paneManager.splitPane(id, direction);
56904
+ this.runLifecycleHooks({
56905
+ phase: "before",
56906
+ action: "split-pane",
56907
+ paneId: id,
56908
+ direction
56909
+ });
56910
+ const pane = this.paneManager.splitPane(id, direction);
56911
+ this.runLifecycleHooks({
56912
+ phase: "after",
56913
+ action: "split-pane",
56914
+ sourcePaneId: id,
56915
+ createdPaneId: pane?.id ?? null,
56916
+ direction,
56917
+ ok: !!pane
56918
+ });
56919
+ return pane;
56572
56920
  }
56573
56921
  closePane(id) {
56574
- return this.paneManager.closePane(id);
56922
+ this.runLifecycleHooks({ phase: "before", action: "close-pane", paneId: id });
56923
+ const ok = this.paneManager.closePane(id);
56924
+ this.runLifecycleHooks({
56925
+ phase: "after",
56926
+ action: "close-pane",
56927
+ paneId: id,
56928
+ ok
56929
+ });
56930
+ return ok;
56575
56931
  }
56576
56932
  getPaneStyleOptions() {
56577
56933
  return this.paneManager.getStyleOptions();
@@ -56580,10 +56936,34 @@ class Restty {
56580
56936
  this.paneManager.setStyleOptions(options);
56581
56937
  }
56582
56938
  setActivePane(id, options) {
56939
+ this.runLifecycleHooks({
56940
+ phase: "before",
56941
+ action: "set-active-pane",
56942
+ paneId: id
56943
+ });
56583
56944
  this.paneManager.setActivePane(id, options);
56945
+ const activePaneId = this.getActivePane()?.id ?? null;
56946
+ this.runLifecycleHooks({
56947
+ phase: "after",
56948
+ action: "set-active-pane",
56949
+ paneId: activePaneId,
56950
+ ok: activePaneId === id
56951
+ });
56584
56952
  }
56585
56953
  markPaneFocused(id, options) {
56954
+ this.runLifecycleHooks({
56955
+ phase: "before",
56956
+ action: "mark-pane-focused",
56957
+ paneId: id
56958
+ });
56586
56959
  this.paneManager.markPaneFocused(id, options);
56960
+ const focusedPaneId = this.getFocusedPane()?.id ?? null;
56961
+ this.runLifecycleHooks({
56962
+ phase: "after",
56963
+ action: "mark-pane-focused",
56964
+ paneId: focusedPaneId,
56965
+ ok: focusedPaneId === id
56966
+ });
56587
56967
  }
56588
56968
  requestLayoutSync() {
56589
56969
  this.paneManager.requestLayoutSync();
@@ -56591,14 +56971,193 @@ class Restty {
56591
56971
  hideContextMenu() {
56592
56972
  this.paneManager.hideContextMenu();
56593
56973
  }
56974
+ async use(plugin, options) {
56975
+ if (!plugin || typeof plugin !== "object") {
56976
+ throw new Error("Restty plugin must be an object");
56977
+ }
56978
+ const pluginId = plugin.id?.trim?.() ?? "";
56979
+ if (!pluginId) {
56980
+ throw new Error("Restty plugin id is required");
56981
+ }
56982
+ if (typeof plugin.activate !== "function") {
56983
+ throw new Error(`Restty plugin ${pluginId} must define activate(context)`);
56984
+ }
56985
+ if (this.pluginRuntimes.has(pluginId))
56986
+ return;
56987
+ try {
56988
+ this.assertPluginCompatibility(pluginId, plugin);
56989
+ } catch (error) {
56990
+ this.pluginDiagnostics.set(pluginId, {
56991
+ id: pluginId,
56992
+ version: plugin.version?.trim?.() || null,
56993
+ apiVersion: Number.isFinite(plugin.apiVersion) ? Number(plugin.apiVersion) : null,
56994
+ requires: plugin.requires ?? null,
56995
+ active: false,
56996
+ activatedAt: null,
56997
+ lastError: this.errorToMessage(error)
56998
+ });
56999
+ throw error;
57000
+ }
57001
+ const runtime2 = {
57002
+ plugin: this.normalizePluginMetadata(plugin, pluginId),
57003
+ cleanup: null,
57004
+ activatedAt: Date.now(),
57005
+ options,
57006
+ disposers: []
57007
+ };
57008
+ this.pluginDiagnostics.set(pluginId, {
57009
+ id: pluginId,
57010
+ version: runtime2.plugin.version?.trim?.() || null,
57011
+ apiVersion: Number.isFinite(runtime2.plugin.apiVersion) ? Number(runtime2.plugin.apiVersion) : null,
57012
+ requires: runtime2.plugin.requires ?? null,
57013
+ active: false,
57014
+ activatedAt: null,
57015
+ lastError: null
57016
+ });
57017
+ this.pluginRuntimes.set(pluginId, runtime2);
57018
+ try {
57019
+ const cleanup = await runtime2.plugin.activate(this.createPluginContext(runtime2), runtime2.options);
57020
+ runtime2.cleanup = this.normalizePluginCleanup(cleanup);
57021
+ runtime2.activatedAt = Date.now();
57022
+ this.updatePluginDiagnostic(pluginId, {
57023
+ active: true,
57024
+ activatedAt: runtime2.activatedAt,
57025
+ lastError: null
57026
+ });
57027
+ this.emitPluginEvent("plugin:activated", { pluginId });
57028
+ } catch (error) {
57029
+ this.teardownPluginRuntime(runtime2);
57030
+ this.pluginRuntimes.delete(pluginId);
57031
+ this.updatePluginDiagnostic(pluginId, {
57032
+ active: false,
57033
+ activatedAt: null,
57034
+ lastError: this.errorToMessage(error)
57035
+ });
57036
+ throw error;
57037
+ }
57038
+ }
57039
+ async loadPlugins(manifest, registry) {
57040
+ const results = [];
57041
+ for (let i3 = 0;i3 < manifest.length; i3 += 1) {
57042
+ const item = manifest[i3];
57043
+ const pluginId = item.id?.trim?.() ?? "";
57044
+ if (!pluginId) {
57045
+ results.push({
57046
+ id: "",
57047
+ status: "failed",
57048
+ error: "Restty plugin manifest entry is missing id"
57049
+ });
57050
+ continue;
57051
+ }
57052
+ if (item.enabled === false) {
57053
+ results.push({ id: pluginId, status: "skipped", error: null });
57054
+ continue;
57055
+ }
57056
+ const entry = this.lookupPluginRegistryEntry(registry, pluginId);
57057
+ if (!entry) {
57058
+ const message = `Restty plugin ${pluginId} was not found in registry`;
57059
+ this.setPluginLoadError(pluginId, message);
57060
+ results.push({ id: pluginId, status: "missing", error: message });
57061
+ continue;
57062
+ }
57063
+ let plugin;
57064
+ try {
57065
+ plugin = await this.resolvePluginRegistryEntry(entry);
57066
+ } catch (error) {
57067
+ const message = this.errorToMessage(error);
57068
+ this.setPluginLoadError(pluginId, message);
57069
+ results.push({ id: pluginId, status: "failed", error: message });
57070
+ continue;
57071
+ }
57072
+ const resolvedId = plugin.id?.trim?.() ?? "";
57073
+ if (resolvedId !== pluginId) {
57074
+ const message = `Restty plugin registry entry ${pluginId} resolved to id ${resolvedId || "(empty)"}`;
57075
+ this.setPluginLoadError(pluginId, message);
57076
+ results.push({ id: pluginId, status: "failed", error: message });
57077
+ continue;
57078
+ }
57079
+ try {
57080
+ await this.use(plugin, item.options);
57081
+ results.push({ id: pluginId, status: "loaded", error: null });
57082
+ } catch (error) {
57083
+ results.push({
57084
+ id: pluginId,
57085
+ status: "failed",
57086
+ error: this.errorToMessage(error)
57087
+ });
57088
+ }
57089
+ }
57090
+ return results;
57091
+ }
57092
+ unuse(pluginId) {
57093
+ const key = pluginId?.trim?.() ?? "";
57094
+ if (!key)
57095
+ return false;
57096
+ const runtime2 = this.pluginRuntimes.get(key);
57097
+ if (!runtime2)
57098
+ return false;
57099
+ this.pluginRuntimes.delete(key);
57100
+ this.teardownPluginRuntime(runtime2);
57101
+ this.updatePluginDiagnostic(key, {
57102
+ active: false,
57103
+ activatedAt: null
57104
+ });
57105
+ this.emitPluginEvent("plugin:deactivated", { pluginId: key });
57106
+ return true;
57107
+ }
57108
+ plugins() {
57109
+ return Array.from(this.pluginRuntimes.keys());
57110
+ }
57111
+ pluginInfo(pluginId) {
57112
+ if (typeof pluginId === "string") {
57113
+ const key = pluginId.trim();
57114
+ if (!key)
57115
+ return null;
57116
+ return this.buildPluginInfo(key);
57117
+ }
57118
+ const keys = new Set;
57119
+ for (const key of this.pluginDiagnostics.keys())
57120
+ keys.add(key);
57121
+ for (const key of this.pluginRuntimes.keys())
57122
+ keys.add(key);
57123
+ return Array.from(keys).sort((a3, b3) => a3.localeCompare(b3)).map((key) => this.buildPluginInfo(key)).filter((entry) => entry !== null);
57124
+ }
56594
57125
  destroy() {
57126
+ const pluginIds = this.plugins();
57127
+ for (let i3 = 0;i3 < pluginIds.length; i3 += 1) {
57128
+ this.unuse(pluginIds[i3]);
57129
+ }
56595
57130
  this.paneManager.destroy();
56596
57131
  }
56597
57132
  connectPty(url = "") {
56598
- this.requireActivePaneHandle().connectPty(url);
57133
+ const pane = this.requireActivePaneHandle();
57134
+ this.runLifecycleHooks({
57135
+ phase: "before",
57136
+ action: "connect-pty",
57137
+ paneId: pane.id
57138
+ });
57139
+ pane.connectPty(url);
57140
+ this.runLifecycleHooks({
57141
+ phase: "after",
57142
+ action: "connect-pty",
57143
+ paneId: pane.id,
57144
+ ok: true
57145
+ });
56599
57146
  }
56600
57147
  disconnectPty() {
56601
- this.requireActivePaneHandle().disconnectPty();
57148
+ const pane = this.requireActivePaneHandle();
57149
+ this.runLifecycleHooks({
57150
+ phase: "before",
57151
+ action: "disconnect-pty",
57152
+ paneId: pane.id
57153
+ });
57154
+ pane.disconnectPty();
57155
+ this.runLifecycleHooks({
57156
+ phase: "after",
57157
+ action: "disconnect-pty",
57158
+ paneId: pane.id,
57159
+ ok: true
57160
+ });
56602
57161
  }
56603
57162
  isPtyConnected() {
56604
57163
  return this.requireActivePaneHandle().isPtyConnected();
@@ -56645,6 +57204,58 @@ class Restty {
56645
57204
  dumpAtlasForCodepoint(cp) {
56646
57205
  this.requireActivePaneHandle().dumpAtlasForCodepoint(cp);
56647
57206
  }
57207
+ resize(cols, rows) {
57208
+ const pane = this.requireActivePaneHandle();
57209
+ this.runLifecycleHooks({
57210
+ phase: "before",
57211
+ action: "resize",
57212
+ paneId: pane.id,
57213
+ cols,
57214
+ rows
57215
+ });
57216
+ pane.resize(cols, rows);
57217
+ this.runLifecycleHooks({
57218
+ phase: "after",
57219
+ action: "resize",
57220
+ paneId: pane.id,
57221
+ cols,
57222
+ rows,
57223
+ ok: true
57224
+ });
57225
+ this.emitPluginEvent("pane:resized", { paneId: pane.id, cols, rows });
57226
+ }
57227
+ focus() {
57228
+ const pane = this.requireActivePaneHandle();
57229
+ this.runLifecycleHooks({
57230
+ phase: "before",
57231
+ action: "focus",
57232
+ paneId: pane.id
57233
+ });
57234
+ pane.focus();
57235
+ this.runLifecycleHooks({
57236
+ phase: "after",
57237
+ action: "focus",
57238
+ paneId: pane.id,
57239
+ ok: true
57240
+ });
57241
+ this.emitPluginEvent("pane:focused", { paneId: pane.id });
57242
+ }
57243
+ blur() {
57244
+ const pane = this.requireActivePaneHandle();
57245
+ this.runLifecycleHooks({
57246
+ phase: "before",
57247
+ action: "blur",
57248
+ paneId: pane.id
57249
+ });
57250
+ pane.blur();
57251
+ this.runLifecycleHooks({
57252
+ phase: "after",
57253
+ action: "blur",
57254
+ paneId: pane.id,
57255
+ ok: true
57256
+ });
57257
+ this.emitPluginEvent("pane:blurred", { paneId: pane.id });
57258
+ }
56648
57259
  updateSize(force) {
56649
57260
  this.requireActivePaneHandle().updateSize(force);
56650
57261
  }
@@ -56667,9 +57278,300 @@ class Restty {
56667
57278
  }
56668
57279
  return this.makePaneHandle(pane.id);
56669
57280
  }
57281
+ createPluginContext(runtime2) {
57282
+ return {
57283
+ restty: this,
57284
+ options: runtime2.options,
57285
+ panes: () => this.panes(),
57286
+ pane: (id) => this.pane(id),
57287
+ activePane: () => this.activePane(),
57288
+ focusedPane: () => this.focusedPane(),
57289
+ on: (event, listener) => {
57290
+ return {
57291
+ dispose: this.attachRuntimeDisposer(runtime2, "event", this.onPluginEvent(event, listener))
57292
+ };
57293
+ },
57294
+ addInputInterceptor: (interceptor, options) => {
57295
+ return {
57296
+ dispose: this.attachRuntimeDisposer(runtime2, "input-interceptor", this.addInputInterceptor(runtime2.plugin.id, interceptor, options))
57297
+ };
57298
+ },
57299
+ addOutputInterceptor: (interceptor, options) => {
57300
+ return {
57301
+ dispose: this.attachRuntimeDisposer(runtime2, "output-interceptor", this.addOutputInterceptor(runtime2.plugin.id, interceptor, options))
57302
+ };
57303
+ },
57304
+ addLifecycleHook: (hook, options) => {
57305
+ return {
57306
+ dispose: this.attachRuntimeDisposer(runtime2, "lifecycle-hook", this.addLifecycleHook(runtime2.plugin.id, hook, options))
57307
+ };
57308
+ },
57309
+ addRenderHook: (hook, options) => {
57310
+ return {
57311
+ dispose: this.attachRuntimeDisposer(runtime2, "render-hook", this.addRenderHook(runtime2.plugin.id, hook, options))
57312
+ };
57313
+ }
57314
+ };
57315
+ }
57316
+ attachRuntimeDisposer(runtime2, kind, dispose) {
57317
+ const entry = {
57318
+ kind,
57319
+ active: true,
57320
+ dispose: () => {
57321
+ if (!entry.active)
57322
+ return;
57323
+ entry.active = false;
57324
+ dispose();
57325
+ }
57326
+ };
57327
+ runtime2.disposers.push(entry);
57328
+ return entry.dispose;
57329
+ }
57330
+ addInputInterceptor(pluginId, interceptor, options) {
57331
+ return this.registerInterceptor(this.inputInterceptors, pluginId, interceptor, options);
57332
+ }
57333
+ addOutputInterceptor(pluginId, interceptor, options) {
57334
+ return this.registerInterceptor(this.outputInterceptors, pluginId, interceptor, options);
57335
+ }
57336
+ addLifecycleHook(pluginId, hook, options) {
57337
+ return this.registerInterceptor(this.lifecycleHooks, pluginId, hook, options);
57338
+ }
57339
+ addRenderHook(pluginId, hook, options) {
57340
+ return this.registerInterceptor(this.renderHooks, pluginId, hook, options);
57341
+ }
57342
+ registerInterceptor(bucket, pluginId, interceptor, options) {
57343
+ const entry = {
57344
+ id: this.nextInterceptorId++,
57345
+ pluginId,
57346
+ priority: Number.isFinite(options?.priority) ? Number(options?.priority) : 0,
57347
+ order: this.nextInterceptorOrder++,
57348
+ interceptor
57349
+ };
57350
+ bucket.push(entry);
57351
+ bucket.sort((a3, b3) => {
57352
+ if (a3.priority !== b3.priority)
57353
+ return a3.priority - b3.priority;
57354
+ return a3.order - b3.order;
57355
+ });
57356
+ return () => {
57357
+ const index = bucket.findIndex((current) => current.id === entry.id);
57358
+ if (index >= 0) {
57359
+ bucket.splice(index, 1);
57360
+ }
57361
+ };
57362
+ }
57363
+ applyInputInterceptors(paneId, text, source) {
57364
+ return this.applyInterceptors(this.inputInterceptors, "input", { paneId, text, source });
57365
+ }
57366
+ applyOutputInterceptors(paneId, text, source) {
57367
+ return this.applyInterceptors(this.outputInterceptors, "output", { paneId, text, source });
57368
+ }
57369
+ runLifecycleHooks(payload) {
57370
+ this.runHooks(this.lifecycleHooks, "lifecycle", payload);
57371
+ }
57372
+ runRenderHooks(payload) {
57373
+ this.runHooks(this.renderHooks, "render", payload);
57374
+ }
57375
+ applyInterceptors(bucket, kind, payload) {
57376
+ let currentText = payload.text;
57377
+ for (let i3 = 0;i3 < bucket.length; i3 += 1) {
57378
+ const entry = bucket[i3];
57379
+ try {
57380
+ const result = entry.interceptor({ ...payload, text: currentText });
57381
+ if (result === null)
57382
+ return null;
57383
+ if (typeof result === "string")
57384
+ currentText = result;
57385
+ } catch (error) {
57386
+ console.error(`[restty plugin] ${kind} interceptor error (${entry.pluginId}):`, error);
57387
+ }
57388
+ }
57389
+ return currentText;
57390
+ }
57391
+ runHooks(bucket, kind, payload) {
57392
+ for (let i3 = 0;i3 < bucket.length; i3 += 1) {
57393
+ const entry = bucket[i3];
57394
+ try {
57395
+ entry.interceptor(payload);
57396
+ } catch (error) {
57397
+ console.error(`[restty plugin] ${kind} hook error (${entry.pluginId}):`, error);
57398
+ }
57399
+ }
57400
+ }
57401
+ normalizePluginMetadata(plugin, pluginId) {
57402
+ return {
57403
+ ...plugin,
57404
+ id: pluginId,
57405
+ version: plugin.version?.trim?.() || undefined,
57406
+ apiVersion: Number.isFinite(plugin.apiVersion) ? Math.trunc(Number(plugin.apiVersion)) : undefined,
57407
+ requires: plugin.requires ?? undefined
57408
+ };
57409
+ }
57410
+ assertPluginCompatibility(pluginId, plugin) {
57411
+ const version = plugin.version?.trim?.();
57412
+ if (version !== undefined && !version) {
57413
+ throw new Error(`Restty plugin ${pluginId} has an empty version`);
57414
+ }
57415
+ if (plugin.apiVersion !== undefined) {
57416
+ if (!Number.isInteger(plugin.apiVersion) || plugin.apiVersion < 1) {
57417
+ throw new Error(`Restty plugin ${pluginId} has invalid apiVersion ${String(plugin.apiVersion)}`);
57418
+ }
57419
+ if (plugin.apiVersion !== RESTTY_PLUGIN_API_VERSION) {
57420
+ throw new Error(`Restty plugin ${pluginId} requires apiVersion ${plugin.apiVersion}, current is ${RESTTY_PLUGIN_API_VERSION}`);
57421
+ }
57422
+ }
57423
+ const requirement = plugin.requires?.pluginApi;
57424
+ if (requirement === undefined)
57425
+ return;
57426
+ if (typeof requirement === "number") {
57427
+ if (!Number.isInteger(requirement) || requirement < 1) {
57428
+ throw new Error(`Restty plugin ${pluginId} has invalid requires.pluginApi value`);
57429
+ }
57430
+ if (requirement !== RESTTY_PLUGIN_API_VERSION) {
57431
+ throw new Error(`Restty plugin ${pluginId} requires pluginApi ${requirement}, current is ${RESTTY_PLUGIN_API_VERSION}`);
57432
+ }
57433
+ return;
57434
+ }
57435
+ const min = requirement.min;
57436
+ const max = requirement.max;
57437
+ if (!Number.isInteger(min) || min < 1) {
57438
+ throw new Error(`Restty plugin ${pluginId} has invalid requires.pluginApi.min`);
57439
+ }
57440
+ if (max !== undefined && (!Number.isInteger(max) || max < min)) {
57441
+ throw new Error(`Restty plugin ${pluginId} has invalid requires.pluginApi.max`);
57442
+ }
57443
+ if (RESTTY_PLUGIN_API_VERSION < min || max !== undefined && RESTTY_PLUGIN_API_VERSION > max) {
57444
+ const range = max === undefined ? `>=${min}` : `${min}-${max}`;
57445
+ throw new Error(`Restty plugin ${pluginId} requires pluginApi range ${range}, current is ${RESTTY_PLUGIN_API_VERSION}`);
57446
+ }
57447
+ }
57448
+ lookupPluginRegistryEntry(registry, pluginId) {
57449
+ if (registry instanceof Map) {
57450
+ return registry.get(pluginId) ?? null;
57451
+ }
57452
+ if (Object.prototype.hasOwnProperty.call(registry, pluginId)) {
57453
+ return registry[pluginId];
57454
+ }
57455
+ return null;
57456
+ }
57457
+ async resolvePluginRegistryEntry(entry) {
57458
+ if (typeof entry === "function") {
57459
+ return await entry();
57460
+ }
57461
+ return entry;
57462
+ }
57463
+ setPluginLoadError(pluginId, message) {
57464
+ this.pluginDiagnostics.set(pluginId, {
57465
+ id: pluginId,
57466
+ version: null,
57467
+ apiVersion: null,
57468
+ requires: null,
57469
+ active: false,
57470
+ activatedAt: null,
57471
+ lastError: message
57472
+ });
57473
+ }
57474
+ updatePluginDiagnostic(pluginId, patch) {
57475
+ const current = this.pluginDiagnostics.get(pluginId);
57476
+ if (!current)
57477
+ return;
57478
+ this.pluginDiagnostics.set(pluginId, {
57479
+ ...current,
57480
+ ...patch
57481
+ });
57482
+ }
57483
+ buildPluginInfo(pluginId) {
57484
+ const diagnostic = this.pluginDiagnostics.get(pluginId) ?? null;
57485
+ const runtime2 = this.pluginRuntimes.get(pluginId) ?? null;
57486
+ if (!diagnostic && !runtime2)
57487
+ return null;
57488
+ const plugin = runtime2?.plugin;
57489
+ const listeners = runtime2 ? runtime2.disposers.filter((entry) => entry.active && entry.kind === "event").length : 0;
57490
+ const inputInterceptors = runtime2 ? runtime2.disposers.filter((entry) => entry.active && entry.kind === "input-interceptor").length : 0;
57491
+ const outputInterceptors = runtime2 ? runtime2.disposers.filter((entry) => entry.active && entry.kind === "output-interceptor").length : 0;
57492
+ const lifecycleHooks = runtime2 ? runtime2.disposers.filter((entry) => entry.active && entry.kind === "lifecycle-hook").length : 0;
57493
+ const renderHooks = runtime2 ? runtime2.disposers.filter((entry) => entry.active && entry.kind === "render-hook").length : 0;
57494
+ return {
57495
+ id: pluginId,
57496
+ version: plugin?.version?.trim?.() || diagnostic?.version || null,
57497
+ apiVersion: plugin?.apiVersion ?? (Number.isFinite(diagnostic?.apiVersion) ? diagnostic?.apiVersion : null),
57498
+ requires: plugin?.requires ?? diagnostic?.requires ?? null,
57499
+ active: runtime2 ? true : diagnostic?.active ?? false,
57500
+ activatedAt: runtime2?.activatedAt ?? diagnostic?.activatedAt ?? null,
57501
+ lastError: diagnostic?.lastError ?? null,
57502
+ listeners,
57503
+ inputInterceptors,
57504
+ outputInterceptors,
57505
+ lifecycleHooks,
57506
+ renderHooks
57507
+ };
57508
+ }
57509
+ errorToMessage(error) {
57510
+ if (error instanceof Error)
57511
+ return error.message || error.name || "Unknown error";
57512
+ return String(error);
57513
+ }
57514
+ onPluginEvent(event, listener) {
57515
+ let listeners = this.pluginListeners.get(event);
57516
+ if (!listeners) {
57517
+ listeners = new Set;
57518
+ this.pluginListeners.set(event, listeners);
57519
+ }
57520
+ const wrapped = listener;
57521
+ listeners.add(wrapped);
57522
+ return () => {
57523
+ const current = this.pluginListeners.get(event);
57524
+ if (!current)
57525
+ return;
57526
+ current.delete(wrapped);
57527
+ if (current.size === 0) {
57528
+ this.pluginListeners.delete(event);
57529
+ }
57530
+ };
57531
+ }
57532
+ emitPluginEvent(event, payload) {
57533
+ const listeners = this.pluginListeners.get(event);
57534
+ if (!listeners || listeners.size === 0)
57535
+ return;
57536
+ const snapshot = Array.from(listeners);
57537
+ for (let i3 = 0;i3 < snapshot.length; i3 += 1) {
57538
+ try {
57539
+ snapshot[i3](payload);
57540
+ } catch (error) {
57541
+ console.error(`[restty plugin] listener error (${String(event)}):`, error);
57542
+ }
57543
+ }
57544
+ }
57545
+ teardownPluginRuntime(runtime2) {
57546
+ for (let i3 = 0;i3 < runtime2.disposers.length; i3 += 1) {
57547
+ try {
57548
+ runtime2.disposers[i3].dispose();
57549
+ } catch {}
57550
+ }
57551
+ runtime2.disposers.length = 0;
57552
+ const cleanup = runtime2.cleanup;
57553
+ runtime2.cleanup = null;
57554
+ if (!cleanup)
57555
+ return;
57556
+ try {
57557
+ cleanup();
57558
+ } catch (error) {
57559
+ console.error(`[restty plugin] cleanup error (${runtime2.plugin.id}):`, error);
57560
+ }
57561
+ }
57562
+ normalizePluginCleanup(cleanup) {
57563
+ if (!cleanup)
57564
+ return null;
57565
+ if (typeof cleanup === "function")
57566
+ return cleanup;
57567
+ if (typeof cleanup === "object" && typeof cleanup.dispose === "function") {
57568
+ return () => cleanup.dispose();
57569
+ }
57570
+ return null;
57571
+ }
56670
57572
  }
56671
57573
  function createRestty(options) {
56672
57574
  return new Restty(options);
56673
57575
  }
56674
57576
 
56675
- export { BOX_LINE_MAP, BOX_STYLE_NONE, BOX_STYLE_LIGHT, BOX_STYLE_HEAVY, BOX_STYLE_DOUBLE, isPrivateUse, isSpaceCp, isBoxDrawing, isBlockElement, isLegacyComputing, isPowerline, isBraille, isGraphicsElement, isSymbolCp, applyAlpha, pushRect, pushRectSnapped, pushRectBox, drawBlockElement, drawBoxDrawing, drawBraille, drawPowerline, constrainGlyphBox, RECT_SHADER, GLYPH_SHADER, initWebGPUCore, initWebGPU, initWebGL, ensureInstanceBuffer, configureContext, ensureGLInstanceBuffer, createResizeState, createScrollbarState, ResttyWasm, loadResttyWasm, createInputHandler, NERD_SYMBOL_RANGES, isNerdSymbolCodepoint, fontHeightUnits, computeCellMetrics, createGridState, updateGridState, clamp, isSymbolFont, isNerdSymbolFont, fontMaxCellSpan, fontScaleOverride, fontRasterScale, createFontEntry, resetFontEntry, createFontManagerState, fontHasGlyph, fontAdvanceUnits, glyphWidthUnits, pickFontIndexForText, tryFetchFontBuffer, tryLocalFontBuffer, loadPrimaryFontBuffer, loadFallbackFontBuffers, NERD_CONSTRAINTS, getNerdConstraint, createPtyConnection, connectPty, disconnectPty, sendPtyInput, sendPtyResize, isPtyConnected, createWebSocketPtyTransport, parseGhosttyColor, colorToFloats, colorToRgbU32, parseGhosttyTheme, listBuiltinThemeNames, isBuiltinThemeName, getBuiltinThemeSource, getBuiltinTheme, createImeState, setPreedit, clearPreedit, startComposition, updateComposition, endComposition, syncImeSelection, updateImePosition, PREEDIT_BG, PREEDIT_ACTIVE_BG, PREEDIT_FG, PREEDIT_UL, PREEDIT_CARET, ResttyPaneHandle, Restty, createRestty };
57577
+ export { BOX_LINE_MAP, BOX_STYLE_NONE, BOX_STYLE_LIGHT, BOX_STYLE_HEAVY, BOX_STYLE_DOUBLE, isPrivateUse, isSpaceCp, isBoxDrawing, isBlockElement, isLegacyComputing, isPowerline, isBraille, isGraphicsElement, isSymbolCp, applyAlpha, pushRect, pushRectSnapped, pushRectBox, drawBlockElement, drawBoxDrawing, drawBraille, drawPowerline, constrainGlyphBox, RECT_SHADER, GLYPH_SHADER, initWebGPUCore, initWebGPU, initWebGL, ensureInstanceBuffer, configureContext, ensureGLInstanceBuffer, createResizeState, createScrollbarState, ResttyWasm, loadResttyWasm, createInputHandler, NERD_SYMBOL_RANGES, isNerdSymbolCodepoint, fontHeightUnits, computeCellMetrics, createGridState, updateGridState, clamp, isSymbolFont, isNerdSymbolFont, fontMaxCellSpan, fontScaleOverride, fontRasterScale, createFontEntry, resetFontEntry, createFontManagerState, fontHasGlyph, fontAdvanceUnits, glyphWidthUnits, pickFontIndexForText, tryFetchFontBuffer, tryLocalFontBuffer, loadPrimaryFontBuffer, loadFallbackFontBuffers, NERD_CONSTRAINTS, getNerdConstraint, createPtyConnection, connectPty, disconnectPty, sendPtyInput, sendPtyResize, isPtyConnected, createWebSocketPtyTransport, parseGhosttyColor, colorToFloats, colorToRgbU32, parseGhosttyTheme, listBuiltinThemeNames, isBuiltinThemeName, getBuiltinThemeSource, getBuiltinTheme, createImeState, setPreedit, clearPreedit, startComposition, updateComposition, endComposition, syncImeSelection, updateImePosition, PREEDIT_BG, PREEDIT_ACTIVE_BG, PREEDIT_FG, PREEDIT_UL, PREEDIT_CARET, RESTTY_PLUGIN_API_VERSION, ResttyPaneHandle, Restty, createRestty };