silvery 0.21.0 → 0.23.0
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.
- package/README.md +1 -1
- package/dist/{Text-Lq0dmj8-.mjs → Text-Ci6UCSTL.mjs} +46 -48
- package/dist/Text-Ci6UCSTL.mjs.map +1 -0
- package/dist/{render-string-DkQacASz.mjs → ag-q9lRlKlt.mjs} +5322 -4259
- package/dist/ag-q9lRlKlt.mjs.map +1 -0
- package/dist/{animation-ZMN2_XKv.mjs → animation-CZ4x2R7C.mjs} +1 -1
- package/dist/{animation-ZMN2_XKv.mjs.map → animation-CZ4x2R7C.mjs.map} +1 -1
- package/dist/{ansi-D1KQMAbf.mjs → ansi-CLmIMnop.mjs} +1 -1
- package/dist/{ansi-D1KQMAbf.mjs.map → ansi-CLmIMnop.mjs.map} +1 -1
- package/dist/{ansi-2Xn0yatP.d.mts → ansi-zmNzgkPB.d.mts} +1 -1
- package/dist/{ansi-2Xn0yatP.d.mts.map → ansi-zmNzgkPB.d.mts.map} +1 -1
- package/dist/{backend-B-WYLUib.mjs → backend-DSiSIc-h.mjs} +2 -2
- package/dist/{backend-B-WYLUib.mjs.map → backend-DSiSIc-h.mjs.map} +1 -1
- package/dist/chain-bridge-CkYcMxlq.mjs +4149 -0
- package/dist/chain-bridge-CkYcMxlq.mjs.map +1 -0
- package/dist/{chunk-BSw8zbkd.mjs → chunk-BEJ448es.mjs} +1 -3
- package/dist/cli-VNqUKlXw.mjs +4 -0
- package/dist/{context-BU5LkkIy.mjs → context-CEhuD1R_.mjs} +1 -1
- package/dist/{context-BU5LkkIy.mjs.map → context-CEhuD1R_.mjs.map} +1 -1
- package/dist/devtools-BF4IMumr.mjs +2 -0
- package/dist/{devtools-DcQjgyjL.mjs → devtools-td2AjFwc.mjs} +5 -5
- package/dist/{devtools-DcQjgyjL.mjs.map → devtools-td2AjFwc.mjs.map} +1 -1
- package/dist/{easing-BI-ASGMO.d.mts → easing-C5MrdO7V.d.mts} +1 -1
- package/dist/{easing-BI-ASGMO.d.mts.map → easing-C5MrdO7V.d.mts.map} +1 -1
- package/dist/{eta-CJlGH06n.mjs → eta-BwYWEjfS.mjs} +1 -1
- package/dist/{eta-CJlGH06n.mjs.map → eta-BwYWEjfS.mjs.map} +1 -1
- package/dist/{flexily-zero-adapter-C4lW_Ov5.mjs → flexily-zero-adapter-DktX3HsX.mjs} +1 -1
- package/dist/{flexily-zero-adapter-C3Vj0fPt.mjs → flexily-zero-adapter-VoBGHG14.mjs} +1 -1
- package/dist/{flexily-zero-adapter-C3Vj0fPt.mjs.map → flexily-zero-adapter-VoBGHG14.mjs.map} +1 -1
- package/dist/hit-registry-D_nxOivS.d.mts +71 -0
- package/dist/hit-registry-D_nxOivS.d.mts.map +1 -0
- package/dist/{UPNG-DosRPdF4.mjs → image-CIF75xMr.mjs} +891 -6
- package/dist/image-CIF75xMr.mjs.map +1 -0
- package/dist/{index-CSQf13CI.d.mts → index-377zdPLP.d.mts} +35 -12
- package/dist/index-377zdPLP.d.mts.map +1 -0
- package/dist/{index-Cl9KKjQ_.d.mts → index-BcSr0RAQ.d.mts} +8670 -10659
- package/dist/index-BcSr0RAQ.d.mts.map +1 -0
- package/dist/{index-XbNrPhWl.d.mts → index-DLnpxDAm.d.mts} +2 -2
- package/dist/{index-XbNrPhWl.d.mts.map → index-DLnpxDAm.d.mts.map} +1 -1
- package/dist/{index-BUMxS65f.d.mts → index-DlNmckTq.d.mts} +2 -2
- package/dist/{index-BUMxS65f.d.mts.map → index-DlNmckTq.d.mts.map} +1 -1
- package/dist/index.d.mts +12 -8
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +18 -15
- package/dist/index.mjs.map +1 -1
- package/dist/{layout-engine-C2px0RJE.mjs → layout-engine-BgR58vL0.mjs} +3 -3
- package/dist/{layout-engine-C2px0RJE.mjs.map → layout-engine-BgR58vL0.mjs.map} +1 -1
- package/dist/{layout-engine-C07LEXWT.mjs → layout-engine-DNjTha8f.mjs} +1 -1
- package/dist/{layout-signals-Cnw6xk8Q.mjs → layout-signals-CGYbHmxy.mjs} +146 -23
- package/dist/layout-signals-CGYbHmxy.mjs.map +1 -0
- package/dist/{bound-term-0sPrrzH1.d.mts → mouse-events-B2z5tbsL.d.mts} +780 -379
- package/dist/mouse-events-B2z5tbsL.d.mts.map +1 -0
- package/dist/{mouse-events-Dki3ISIp.mjs → mouse-events-WQ2h-uqG.mjs} +45 -10
- package/dist/mouse-events-WQ2h-uqG.mjs.map +1 -0
- package/dist/{multi-progress-DHZ2xUT2.d.mts → multi-progress-BaEFfHKP.d.mts} +2 -2
- package/dist/{multi-progress-DHZ2xUT2.d.mts.map → multi-progress-BaEFfHKP.d.mts.map} +1 -1
- package/dist/{multi-progress-CIRjrzma.mjs → multi-progress-zCWHEm9P.mjs} +3 -3
- package/dist/{multi-progress-CIRjrzma.mjs.map → multi-progress-zCWHEm9P.mjs.map} +1 -1
- package/dist/{node-CjM5Rt-M.mjs → node-CJ3J_rW-.mjs} +1 -1
- package/dist/{node-CjM5Rt-M.mjs.map → node-CJ3J_rW-.mjs.map} +1 -1
- package/dist/{progress-DB_Xo071.mjs → progress-6vJ7nxUg.mjs} +4 -4
- package/dist/{progress-DB_Xo071.mjs.map → progress-6vJ7nxUg.mjs.map} +1 -1
- package/dist/{progress-bar-oJwq22CR.mjs → progress-bar-BlFNsZL8.mjs} +4 -4
- package/dist/{progress-bar-oJwq22CR.mjs.map → progress-bar-BlFNsZL8.mjs.map} +1 -1
- package/dist/{reconciler-DldIJB93.mjs → reconciler--_-biN23.mjs} +190 -134
- package/dist/reconciler--_-biN23.mjs.map +1 -0
- package/dist/{render-string-BcoCpjCB.mjs → render-string-DaghHpLN.mjs} +1 -1
- package/dist/render-string-DtHZP0cU.mjs +215 -0
- package/dist/render-string-DtHZP0cU.mjs.map +1 -0
- package/dist/runtime.d.mts +4 -3
- package/dist/runtime.mjs +4 -3
- package/dist/schemes-DWQPxXAi.mjs +2516 -0
- package/dist/{schemes-JjNp4aSl.mjs.map → schemes-DWQPxXAi.mjs.map} +1 -1
- package/dist/{spinner-D9lrHr8s.mjs → spinner-B50hILqx.mjs} +19 -4
- package/dist/{spinner-D9lrHr8s.mjs.map → spinner-B50hILqx.mjs.map} +1 -1
- package/dist/{spinner-CZINHpkV.d.mts → spinner-DN10UfF0.d.mts} +2 -2
- package/dist/{spinner-CZINHpkV.d.mts.map → spinner-DN10UfF0.d.mts.map} +1 -1
- package/dist/{src-BNTToU7l.mjs → src-B8lf1kiR.mjs} +1507 -1166
- package/dist/src-B8lf1kiR.mjs.map +1 -0
- package/dist/{src-5w9QR6_8.mjs → src-BOLdi3WG.mjs} +105 -237
- package/dist/src-BOLdi3WG.mjs.map +1 -0
- package/dist/{src-BR4xNwdG.mjs → src-BXeTHHlL.mjs} +20240 -25437
- package/dist/src-BXeTHHlL.mjs.map +1 -0
- package/dist/{src-DKp-_OFG.mjs → src-BqeBFB3X.mjs} +90 -47
- package/dist/src-BqeBFB3X.mjs.map +1 -0
- package/dist/src-DbqcSrhy.mjs +3901 -0
- package/dist/src-DbqcSrhy.mjs.map +1 -0
- package/dist/{steps-Bp2uNqnn.d.mts → steps-B2kBmFpb.d.mts} +1 -1
- package/dist/{steps-Bp2uNqnn.d.mts.map → steps-B2kBmFpb.d.mts.map} +1 -1
- package/dist/{svg-Cz0UXcDj.mjs → svg-NmV9dgB9.mjs} +1 -1
- package/dist/{svg-Cz0UXcDj.mjs.map → svg-NmV9dgB9.mjs.map} +1 -1
- package/dist/{svg-g1D6ErwR.d.mts → svg-VUr78l5d.d.mts} +1 -1
- package/dist/{svg-g1D6ErwR.d.mts.map → svg-VUr78l5d.d.mts.map} +1 -1
- package/dist/term.d.mts +2 -2
- package/dist/term.mjs +3 -8
- package/dist/test.d.mts +673 -0
- package/dist/test.d.mts.map +1 -0
- package/dist/test.mjs +2829 -0
- package/dist/test.mjs.map +1 -0
- package/dist/theme.d.mts +2 -2
- package/dist/theme.d.mts.map +1 -1
- package/dist/theme.mjs +3 -8
- package/dist/{types-kt_fKR37.d.mts → types-D_E0FbqA.d.mts} +1 -1
- package/dist/{types-kt_fKR37.d.mts.map → types-D_E0FbqA.d.mts.map} +1 -1
- package/dist/ui/animation.d.mts +2 -2
- package/dist/ui/animation.mjs +1 -1
- package/dist/ui/ansi.d.mts +1 -1
- package/dist/ui/ansi.mjs +1 -1
- package/dist/ui/cli.d.mts +3 -3
- package/dist/ui/cli.mjs +5 -5
- package/dist/ui/image.d.mts +1 -1
- package/dist/ui/image.mjs +1 -1
- package/dist/ui/progress.d.mts +4 -4
- package/dist/ui/progress.mjs +4 -4
- package/dist/ui/react.d.mts +1 -1
- package/dist/ui/react.mjs +2 -2
- package/dist/ui/recording-chrome-react.d.mts +1 -1
- package/dist/ui/recording-chrome-react.mjs +2 -2
- package/dist/ui/recording-chrome.d.mts +1 -1
- package/dist/ui/recording-chrome.mjs +1 -1
- package/dist/ui/utils.mjs +1 -1
- package/dist/ui/wrappers.d.mts +2 -2
- package/dist/ui/wrappers.mjs +1 -1
- package/dist/ui.d.mts +6 -6
- package/dist/ui.mjs +7 -7
- package/dist/unicode-CxyxVGmB.mjs +11077 -0
- package/dist/unicode-CxyxVGmB.mjs.map +1 -0
- package/dist/{useLatest-DRDDVwjh.d.mts → useLatest-UOvWy0ZW.d.mts} +2 -2
- package/dist/{useLatest-DRDDVwjh.d.mts.map → useLatest-UOvWy0ZW.d.mts.map} +1 -1
- package/dist/useLayout-D5cE8OCI.mjs +424 -0
- package/dist/useLayout-D5cE8OCI.mjs.map +1 -0
- package/dist/{with-text-input-YeohVLeo.d.mts → with-text-input-DLoSCBrm.d.mts} +3 -3
- package/dist/{with-text-input-YeohVLeo.d.mts.map → with-text-input-DLoSCBrm.d.mts.map} +1 -1
- package/dist/wrap-measurer-registration-IV2HtcCd.d.mts +3106 -0
- package/dist/wrap-measurer-registration-IV2HtcCd.d.mts.map +1 -0
- package/dist/{wrapper-C70ATkVv.mjs → wrapper-CD29afOs.mjs} +59 -17
- package/dist/wrapper-CD29afOs.mjs.map +1 -0
- package/dist/{wrappers-BCUYITrY.mjs → wrappers-CInBJm44.mjs} +18 -8
- package/dist/wrappers-CInBJm44.mjs.map +1 -0
- package/dist/{yoga-adapter-BnZX1PAY.mjs → yoga-adapter-9uab5Qn-.mjs} +2 -2
- package/dist/{yoga-adapter-BnZX1PAY.mjs.map → yoga-adapter-9uab5Qn-.mjs.map} +1 -1
- package/dist/yoga-adapter-yqTl1aXK.mjs +2 -0
- package/package.json +59 -14
- package/dist/Text-Lq0dmj8-.mjs.map +0 -1
- package/dist/UPNG-Bo33r8rA.mjs +0 -3
- package/dist/UPNG-DosRPdF4.mjs.map +0 -1
- package/dist/__vite-browser-external-2447137e-D_JM6skp.mjs +0 -6
- package/dist/__vite-browser-external-2447137e-D_JM6skp.mjs.map +0 -1
- package/dist/ansi-yC4RyBNY.mjs +0 -22441
- package/dist/ansi-yC4RyBNY.mjs.map +0 -1
- package/dist/apng-CR08rIaH.mjs +0 -58
- package/dist/apng-CR08rIaH.mjs.map +0 -1
- package/dist/apng-DaHfVaVI.mjs +0 -3
- package/dist/assets/resvgjs.darwin-arm64-BtufyGW1.node +0 -0
- package/dist/assets/skia.darwin-arm64-DQs5sT6N.node +0 -0
- package/dist/backends-CUtan80W.mjs +0 -3
- package/dist/backends-DIVYzKqd.mjs +0 -1083
- package/dist/backends-DIVYzKqd.mjs.map +0 -1
- package/dist/bound-term-0sPrrzH1.d.mts.map +0 -1
- package/dist/canvas-1v7dPT-_.mjs +0 -3
- package/dist/canvas-CSuPOMNt.mjs +0 -1442
- package/dist/canvas-CSuPOMNt.mjs.map +0 -1
- package/dist/cli-dvo0r2fs.mjs +0 -4
- package/dist/compare-CQodSH4G.mjs +0 -376
- package/dist/compare-CQodSH4G.mjs.map +0 -1
- package/dist/compare-DHlcxEYA.mjs +0 -3
- package/dist/devtools-CJdt5H0X.mjs +0 -2
- package/dist/fonts-BFmhXDv7.mjs +0 -88
- package/dist/fonts-BFmhXDv7.mjs.map +0 -1
- package/dist/gif-C_AjaT9d.mjs +0 -188
- package/dist/gif-C_AjaT9d.mjs.map +0 -1
- package/dist/gif-DaC4XrxA.mjs +0 -3
- package/dist/gifenc-BOUT-KFB.mjs +0 -730
- package/dist/gifenc-BOUT-KFB.mjs.map +0 -1
- package/dist/image-C2Birh2x.mjs +0 -1252
- package/dist/image-C2Birh2x.mjs.map +0 -1
- package/dist/index-CSQf13CI.d.mts.map +0 -1
- package/dist/index-Cl9KKjQ_.d.mts.map +0 -1
- package/dist/key-mapping-CS-YD_cD.mjs +0 -132
- package/dist/key-mapping-CS-YD_cD.mjs.map +0 -1
- package/dist/key-mapping-Yn-Jgrij.mjs +0 -3
- package/dist/layout-signals-Cnw6xk8Q.mjs.map +0 -1
- package/dist/mouse-events-Dki3ISIp.mjs.map +0 -1
- package/dist/playwright-D5YiZcNS.mjs +0 -76397
- package/dist/playwright-D5YiZcNS.mjs.map +0 -1
- package/dist/png-codec-Dp84742B.mjs +0 -36
- package/dist/png-codec-Dp84742B.mjs.map +0 -1
- package/dist/png-codec-QwOtJ8Zs.mjs +0 -3
- package/dist/rasterizer-BRXrDdWx.mjs +0 -3
- package/dist/rasterizer-CpEhJvdR.mjs +0 -296
- package/dist/rasterizer-CpEhJvdR.mjs.map +0 -1
- package/dist/reconciler-DldIJB93.mjs.map +0 -1
- package/dist/render-string-DkQacASz.mjs.map +0 -1
- package/dist/resvg-js-DkOndZI3.mjs +0 -203
- package/dist/resvg-js-DkOndZI3.mjs.map +0 -1
- package/dist/schemes-JjNp4aSl.mjs +0 -2611
- package/dist/src-5w9QR6_8.mjs.map +0 -1
- package/dist/src-BNTToU7l.mjs.map +0 -1
- package/dist/src-BR4xNwdG.mjs.map +0 -1
- package/dist/src-DKp-_OFG.mjs.map +0 -1
- package/dist/src-bt8wSrfJ.mjs +0 -258
- package/dist/src-bt8wSrfJ.mjs.map +0 -1
- package/dist/src-e33Y6kNJ.mjs +0 -3
- package/dist/src-iDwu25UD.mjs +0 -1814
- package/dist/src-iDwu25UD.mjs.map +0 -1
- package/dist/svg-15lZZzxq.mjs +0 -486
- package/dist/svg-15lZZzxq.mjs.map +0 -1
- package/dist/svg-DY72a4HK.mjs +0 -3
- package/dist/term.mjs.map +0 -1
- package/dist/theme.mjs.map +0 -1
- package/dist/ui/display.d.mts +0 -35
- package/dist/ui/display.d.mts.map +0 -1
- package/dist/ui/display.mjs +0 -123
- package/dist/ui/display.mjs.map +0 -1
- package/dist/ui/input.d.mts +0 -184
- package/dist/ui/input.d.mts.map +0 -1
- package/dist/ui/input.mjs +0 -285
- package/dist/ui/input.mjs.map +0 -1
- package/dist/wrapper-C70ATkVv.mjs.map +0 -1
- package/dist/wrappers-BCUYITrY.mjs.map +0 -1
- package/dist/yoga-adapter-DxgsQ_gg.mjs +0 -2
- package/dist/zipBundle-3nqeDRtm.mjs +0 -3
- package/dist/zipBundle-VNAYFmqJ.mjs +0 -2003
- package/dist/zipBundle-VNAYFmqJ.mjs.map +0 -1
|
@@ -0,0 +1,3901 @@
|
|
|
1
|
+
import { C as findFocusableAncestor, D as isFocusHidden, E as getTabOrder$1, O as isFocusable, T as getExplicitFocusLink, b as setFocused, w as findSpatialTarget, x as findByTestID } from "./mouse-events-WQ2h-uqG.mjs";
|
|
2
|
+
import { f as syncFocusedSignal } from "./layout-signals-CGYbHmxy.mjs";
|
|
3
|
+
import { N as stripAnsi, Rt as queryKittyKeyboard, U as getInternalStreams, mt as createOutputPhase, r as createMeasurer } from "./unicode-CxyxVGmB.mjs";
|
|
4
|
+
import { j as createTerminalProfile } from "./src-B8lf1kiR.mjs";
|
|
5
|
+
import { C as isDirty } from "./reconciler--_-biN23.mjs";
|
|
6
|
+
import "./layout-engine-BgR58vL0.mjs";
|
|
7
|
+
import { a as setActiveColorLevel, i as parseColor } from "./ag-q9lRlKlt.mjs";
|
|
8
|
+
import { a as NodeContext } from "./context-CEhuD1R_.mjs";
|
|
9
|
+
import { u as useScrollRect } from "./useLayout-D5cE8OCI.mjs";
|
|
10
|
+
import { createLogger } from "loggily";
|
|
11
|
+
import React, { createContext, useContext, useEffect, useLayoutEffect, useRef } from "react";
|
|
12
|
+
//#region packages/ag/src/focus-manager.ts
|
|
13
|
+
function createFocusManager(options) {
|
|
14
|
+
const onFocusChange = options?.onFocusChange;
|
|
15
|
+
let activeElement = null;
|
|
16
|
+
let activeId = null;
|
|
17
|
+
let previousElement = null;
|
|
18
|
+
let previousId = null;
|
|
19
|
+
let focusOrigin = null;
|
|
20
|
+
const scopeStack = [];
|
|
21
|
+
const scopeMemory = {};
|
|
22
|
+
let activeScopeId = null;
|
|
23
|
+
const hookFocusables = [];
|
|
24
|
+
let hookFocusEnabled = true;
|
|
25
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
26
|
+
let snapshot = null;
|
|
27
|
+
/** Counter incremented on every notify(); used by activateScope to detect inner notifications. */
|
|
28
|
+
let notifyCount = 0;
|
|
29
|
+
function notify() {
|
|
30
|
+
snapshot = null;
|
|
31
|
+
notifyCount++;
|
|
32
|
+
for (const listener of listeners) listener();
|
|
33
|
+
}
|
|
34
|
+
function getTestID(node) {
|
|
35
|
+
const props = node.props;
|
|
36
|
+
return typeof props.testID === "string" ? props.testID : null;
|
|
37
|
+
}
|
|
38
|
+
function focus(node, origin = "programmatic") {
|
|
39
|
+
if (activeElement === node) {
|
|
40
|
+
if (focusOrigin !== origin) {
|
|
41
|
+
focusOrigin = origin;
|
|
42
|
+
notify();
|
|
43
|
+
}
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const oldElement = activeElement;
|
|
47
|
+
previousElement = activeElement;
|
|
48
|
+
previousId = activeId;
|
|
49
|
+
activeElement = node;
|
|
50
|
+
activeId = getTestID(node);
|
|
51
|
+
focusOrigin = origin;
|
|
52
|
+
if (oldElement) {
|
|
53
|
+
setFocused(oldElement, false);
|
|
54
|
+
syncFocusedSignal(oldElement, false);
|
|
55
|
+
}
|
|
56
|
+
setFocused(node, true);
|
|
57
|
+
syncFocusedSignal(node, true);
|
|
58
|
+
if (activeId && scopeStack.length > 0) scopeMemory[scopeStack[scopeStack.length - 1]] = activeId;
|
|
59
|
+
notify();
|
|
60
|
+
onFocusChange?.(oldElement, node, origin);
|
|
61
|
+
}
|
|
62
|
+
function focusById(id, root, origin = "programmatic") {
|
|
63
|
+
const node = findByTestID(root, id);
|
|
64
|
+
if (node) {
|
|
65
|
+
const focusable = findFocusableAncestor(node);
|
|
66
|
+
if (focusable) {
|
|
67
|
+
focus(focusable, origin);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (activeId === id && !activeElement) return;
|
|
72
|
+
const oldElement = activeElement;
|
|
73
|
+
previousElement = activeElement;
|
|
74
|
+
previousId = activeId;
|
|
75
|
+
activeElement = null;
|
|
76
|
+
activeId = id;
|
|
77
|
+
focusOrigin = origin;
|
|
78
|
+
if (oldElement) {
|
|
79
|
+
setFocused(oldElement, false);
|
|
80
|
+
syncFocusedSignal(oldElement, false);
|
|
81
|
+
}
|
|
82
|
+
notify();
|
|
83
|
+
onFocusChange?.(oldElement, null, origin);
|
|
84
|
+
}
|
|
85
|
+
function blur() {
|
|
86
|
+
if (!activeElement && !activeId) return;
|
|
87
|
+
const oldElement = activeElement;
|
|
88
|
+
previousElement = activeElement;
|
|
89
|
+
previousId = activeId;
|
|
90
|
+
activeElement = null;
|
|
91
|
+
activeId = null;
|
|
92
|
+
focusOrigin = null;
|
|
93
|
+
if (oldElement) {
|
|
94
|
+
setFocused(oldElement, false);
|
|
95
|
+
syncFocusedSignal(oldElement, false);
|
|
96
|
+
}
|
|
97
|
+
notify();
|
|
98
|
+
onFocusChange?.(oldElement, null, null);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Set the focused target to a hook-registered id directly (no tree lookup).
|
|
102
|
+
* activeElement becomes null (no backing node), activeId is the hook id.
|
|
103
|
+
*/
|
|
104
|
+
function focusVirtualId(id, origin = "programmatic") {
|
|
105
|
+
if (activeId === id && !activeElement) {
|
|
106
|
+
if (focusOrigin !== origin) {
|
|
107
|
+
focusOrigin = origin;
|
|
108
|
+
notify();
|
|
109
|
+
}
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const oldElement = activeElement;
|
|
113
|
+
previousElement = activeElement;
|
|
114
|
+
previousId = activeId;
|
|
115
|
+
activeElement = null;
|
|
116
|
+
activeId = id;
|
|
117
|
+
focusOrigin = origin;
|
|
118
|
+
if (oldElement) {
|
|
119
|
+
setFocused(oldElement, false);
|
|
120
|
+
syncFocusedSignal(oldElement, false);
|
|
121
|
+
}
|
|
122
|
+
notify();
|
|
123
|
+
onFocusChange?.(oldElement, null, origin);
|
|
124
|
+
}
|
|
125
|
+
function unregisterHookFocusable(id) {
|
|
126
|
+
const idx = hookFocusables.findIndex((f) => f.id === id);
|
|
127
|
+
if (idx === -1) return;
|
|
128
|
+
hookFocusables.splice(idx, 1);
|
|
129
|
+
if (activeId === id && !activeElement) {
|
|
130
|
+
previousId = activeId;
|
|
131
|
+
activeId = null;
|
|
132
|
+
focusOrigin = null;
|
|
133
|
+
notify();
|
|
134
|
+
onFocusChange?.(null, null, null);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
notify();
|
|
138
|
+
}
|
|
139
|
+
function registerHookFocusable(id, opts = {}) {
|
|
140
|
+
const { isActive = true, autoFocus = false } = opts;
|
|
141
|
+
const existing = hookFocusables.findIndex((f) => f.id === id);
|
|
142
|
+
if (existing !== -1) {
|
|
143
|
+
if (hookFocusables[existing].isActive === isActive && !autoFocus) return () => unregisterHookFocusable(id);
|
|
144
|
+
hookFocusables[existing] = {
|
|
145
|
+
id,
|
|
146
|
+
isActive
|
|
147
|
+
};
|
|
148
|
+
} else hookFocusables.push({
|
|
149
|
+
id,
|
|
150
|
+
isActive
|
|
151
|
+
});
|
|
152
|
+
if (autoFocus && isActive && activeId === null) focusVirtualId(id, "programmatic");
|
|
153
|
+
else notify();
|
|
154
|
+
return () => unregisterHookFocusable(id);
|
|
155
|
+
}
|
|
156
|
+
function setHookFocusableActive(id, isActive) {
|
|
157
|
+
const entry = hookFocusables.find((f) => f.id === id);
|
|
158
|
+
if (!entry) return;
|
|
159
|
+
if (entry.isActive === isActive) return;
|
|
160
|
+
entry.isActive = isActive;
|
|
161
|
+
if (!isActive && activeId === id && !activeElement) {
|
|
162
|
+
previousId = activeId;
|
|
163
|
+
activeId = null;
|
|
164
|
+
focusOrigin = null;
|
|
165
|
+
notify();
|
|
166
|
+
onFocusChange?.(null, null, null);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
notify();
|
|
170
|
+
}
|
|
171
|
+
function setHookFocusEnabled(enabled) {
|
|
172
|
+
if (hookFocusEnabled === enabled) return;
|
|
173
|
+
hookFocusEnabled = enabled;
|
|
174
|
+
notify();
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Check if a node is the given target or contains it as a descendant.
|
|
178
|
+
*/
|
|
179
|
+
function subtreeContains(subtreeRoot, target) {
|
|
180
|
+
if (subtreeRoot === target) return true;
|
|
181
|
+
for (const child of subtreeRoot.children) if (subtreeContains(child, target)) return true;
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
function updateInvalidatesFocusedNode(updatedNode, focusedNode) {
|
|
185
|
+
if (!subtreeContains(updatedNode, focusedNode)) return false;
|
|
186
|
+
let current = focusedNode;
|
|
187
|
+
while (current) {
|
|
188
|
+
if (isFocusHidden(current)) return true;
|
|
189
|
+
if (current === updatedNode) break;
|
|
190
|
+
current = current.parent;
|
|
191
|
+
}
|
|
192
|
+
return updatedNode === focusedNode && !isFocusable(focusedNode);
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Handle a subtree being removed from the tree. If the active or previous
|
|
196
|
+
* element lives within the removed subtree, clear the dangling reference.
|
|
197
|
+
* This prevents dead node retention and broken navigation (indexOf → -1).
|
|
198
|
+
*/
|
|
199
|
+
function handleSubtreeRemoved(removedRoot) {
|
|
200
|
+
let changed = false;
|
|
201
|
+
if (activeElement && subtreeContains(removedRoot, activeElement)) {
|
|
202
|
+
const oldElement = activeElement;
|
|
203
|
+
setFocused(oldElement, false);
|
|
204
|
+
syncFocusedSignal(oldElement, false);
|
|
205
|
+
previousElement = activeElement;
|
|
206
|
+
previousId = activeId;
|
|
207
|
+
activeElement = null;
|
|
208
|
+
activeId = null;
|
|
209
|
+
focusOrigin = null;
|
|
210
|
+
changed = true;
|
|
211
|
+
onFocusChange?.(oldElement, null, null);
|
|
212
|
+
}
|
|
213
|
+
if (previousElement && subtreeContains(removedRoot, previousElement)) {
|
|
214
|
+
previousElement = null;
|
|
215
|
+
previousId = null;
|
|
216
|
+
changed = true;
|
|
217
|
+
}
|
|
218
|
+
if (changed) notify();
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Handle a mounted node update. React can update a focused node from
|
|
222
|
+
* `focusable` to non-focusable without unmounting it; leaving that node as
|
|
223
|
+
* `activeElement` keeps the focused dispatch lane alive and routes later keys
|
|
224
|
+
* into an input owner the app has already deactivated.
|
|
225
|
+
*/
|
|
226
|
+
function handleNodeUpdated(updatedNode) {
|
|
227
|
+
let changed = false;
|
|
228
|
+
if (activeElement && updateInvalidatesFocusedNode(updatedNode, activeElement)) {
|
|
229
|
+
const oldElement = activeElement;
|
|
230
|
+
setFocused(oldElement, false);
|
|
231
|
+
syncFocusedSignal(oldElement, false);
|
|
232
|
+
previousElement = activeElement;
|
|
233
|
+
previousId = activeId;
|
|
234
|
+
activeElement = null;
|
|
235
|
+
activeId = null;
|
|
236
|
+
focusOrigin = null;
|
|
237
|
+
changed = true;
|
|
238
|
+
onFocusChange?.(oldElement, null, null);
|
|
239
|
+
}
|
|
240
|
+
if (previousElement && updateInvalidatesFocusedNode(updatedNode, previousElement)) {
|
|
241
|
+
previousElement = null;
|
|
242
|
+
previousId = null;
|
|
243
|
+
changed = true;
|
|
244
|
+
}
|
|
245
|
+
if (changed) notify();
|
|
246
|
+
maybePromoteVirtualFocus(updatedNode);
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Promote pending VIRTUAL focus (activeId set, activeElement null) to the
|
|
250
|
+
* real node when a matching focusable target exists under `searchRoot`.
|
|
251
|
+
* Never steals: a real activeElement short-circuits immediately. Resolution
|
|
252
|
+
* mirrors focusById exactly (findByTestID + nearest focusable ancestor), so
|
|
253
|
+
* promoted focus is indistinguishable from a direct hit.
|
|
254
|
+
*/
|
|
255
|
+
function maybePromoteVirtualFocus(searchRoot) {
|
|
256
|
+
if (activeId === null || activeElement !== null) return;
|
|
257
|
+
const node = findByTestID(searchRoot, activeId);
|
|
258
|
+
if (!node) return;
|
|
259
|
+
const focusable = findFocusableAncestor(node);
|
|
260
|
+
if (!focusable) return;
|
|
261
|
+
focus(focusable, focusOrigin ?? "programmatic");
|
|
262
|
+
}
|
|
263
|
+
function handleSubtreeAttached(attachedRoot) {
|
|
264
|
+
maybePromoteVirtualFocus(attachedRoot);
|
|
265
|
+
}
|
|
266
|
+
function enterScope(scopeId) {
|
|
267
|
+
scopeStack.push(scopeId);
|
|
268
|
+
notify();
|
|
269
|
+
}
|
|
270
|
+
function exitScope() {
|
|
271
|
+
if (scopeStack.pop() === void 0) return;
|
|
272
|
+
notify();
|
|
273
|
+
}
|
|
274
|
+
function activateScope(scopeId, root) {
|
|
275
|
+
if (activeScopeId && activeId) scopeMemory[activeScopeId] = activeId;
|
|
276
|
+
activeScopeId = scopeId;
|
|
277
|
+
const countBefore = notifyCount;
|
|
278
|
+
const remembered = scopeMemory[scopeId];
|
|
279
|
+
if (remembered) focusById(remembered, root, "programmatic");
|
|
280
|
+
else {
|
|
281
|
+
const scopeNode = findByTestID(root, scopeId);
|
|
282
|
+
if (scopeNode) {
|
|
283
|
+
const order = getTabOrder$1(root, scopeNode);
|
|
284
|
+
if (order.length > 0) focus(order[0], "programmatic");
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
if (notifyCount === countBefore) notify();
|
|
288
|
+
}
|
|
289
|
+
function getFocusPath(root) {
|
|
290
|
+
if (!activeElement) return [];
|
|
291
|
+
const path = [];
|
|
292
|
+
let current = activeElement;
|
|
293
|
+
while (current && current !== root.parent) {
|
|
294
|
+
const id = getTestID(current);
|
|
295
|
+
if (id) path.push(id);
|
|
296
|
+
current = current.parent;
|
|
297
|
+
}
|
|
298
|
+
return path;
|
|
299
|
+
}
|
|
300
|
+
function hasFocusWithin(root, testID) {
|
|
301
|
+
if (!activeElement) return false;
|
|
302
|
+
const target = findByTestID(root, testID);
|
|
303
|
+
if (!target) return false;
|
|
304
|
+
let current = activeElement;
|
|
305
|
+
while (current) {
|
|
306
|
+
if (current === target) return true;
|
|
307
|
+
current = current.parent;
|
|
308
|
+
}
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Resolve the effective scope node for tab navigation.
|
|
313
|
+
* If an explicit scope is provided, use it. Otherwise, if the scopeStack
|
|
314
|
+
* is non-empty, find the topmost scope node in the tree by testID.
|
|
315
|
+
*/
|
|
316
|
+
function resolveScope(root, explicitScope) {
|
|
317
|
+
if (explicitScope) return explicitScope;
|
|
318
|
+
if (scopeStack.length > 0) {
|
|
319
|
+
const scopeId = scopeStack[scopeStack.length - 1];
|
|
320
|
+
const scopeNode = findByTestID(root, scopeId);
|
|
321
|
+
if (scopeNode) return scopeNode;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
function buildTabEntries(root, scope) {
|
|
325
|
+
const effectiveScope = resolveScope(root, scope);
|
|
326
|
+
const nodes = getTabOrder$1(root, effectiveScope);
|
|
327
|
+
const entries = nodes.map((node) => ({
|
|
328
|
+
kind: "node",
|
|
329
|
+
node
|
|
330
|
+
}));
|
|
331
|
+
if (!effectiveScope && hookFocusEnabled) {
|
|
332
|
+
const treeIds = new Set(nodes.map((n) => n.props.testID).filter(Boolean));
|
|
333
|
+
for (const entry of hookFocusables) if (entry.isActive && !treeIds.has(entry.id)) entries.push({
|
|
334
|
+
kind: "hook",
|
|
335
|
+
id: entry.id
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
return entries;
|
|
339
|
+
}
|
|
340
|
+
function currentTabIndex(entries) {
|
|
341
|
+
if (activeElement) {
|
|
342
|
+
for (let i = 0; i < entries.length; i++) {
|
|
343
|
+
const e = entries[i];
|
|
344
|
+
if (e.kind === "node" && e.node === activeElement) return i;
|
|
345
|
+
}
|
|
346
|
+
return -1;
|
|
347
|
+
}
|
|
348
|
+
if (activeId) for (let i = 0; i < entries.length; i++) {
|
|
349
|
+
const e = entries[i];
|
|
350
|
+
if (e.kind === "hook" && e.id === activeId) return i;
|
|
351
|
+
}
|
|
352
|
+
return -1;
|
|
353
|
+
}
|
|
354
|
+
function focusTabEntry(entry, origin) {
|
|
355
|
+
if (entry.kind === "node") focus(entry.node, origin);
|
|
356
|
+
else focusVirtualId(entry.id, origin);
|
|
357
|
+
}
|
|
358
|
+
function focusNext(root, scope) {
|
|
359
|
+
const entries = buildTabEntries(root, scope);
|
|
360
|
+
if (entries.length === 0) return;
|
|
361
|
+
const currentIndex = currentTabIndex(entries);
|
|
362
|
+
if (currentIndex === -1) {
|
|
363
|
+
focusTabEntry(entries[0], "keyboard");
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
focusTabEntry(entries[(currentIndex + 1) % entries.length], "keyboard");
|
|
367
|
+
}
|
|
368
|
+
function focusPrev(root, scope) {
|
|
369
|
+
const entries = buildTabEntries(root, scope);
|
|
370
|
+
if (entries.length === 0) return;
|
|
371
|
+
const currentIndex = currentTabIndex(entries);
|
|
372
|
+
if (currentIndex === -1) {
|
|
373
|
+
focusTabEntry(entries[entries.length - 1], "keyboard");
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
focusTabEntry(entries[currentIndex <= 0 ? entries.length - 1 : currentIndex - 1], "keyboard");
|
|
377
|
+
}
|
|
378
|
+
function focusDirection(root, direction, layoutFn) {
|
|
379
|
+
if (!activeElement) return;
|
|
380
|
+
const explicitTarget = getExplicitFocusLink(activeElement, direction);
|
|
381
|
+
if (explicitTarget) {
|
|
382
|
+
focusById(explicitTarget, root, "keyboard");
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
const candidates = getTabOrder$1(root);
|
|
386
|
+
const target = findSpatialTarget(activeElement, direction, candidates, layoutFn ?? ((node) => node.scrollRect));
|
|
387
|
+
if (target) focus(target, "keyboard");
|
|
388
|
+
}
|
|
389
|
+
function subscribe(listener) {
|
|
390
|
+
listeners.add(listener);
|
|
391
|
+
return () => {
|
|
392
|
+
listeners.delete(listener);
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
function getSnapshot() {
|
|
396
|
+
if (!snapshot) snapshot = {
|
|
397
|
+
activeId,
|
|
398
|
+
previousId,
|
|
399
|
+
focusOrigin,
|
|
400
|
+
scopeStack: [...scopeStack],
|
|
401
|
+
activeScopeId
|
|
402
|
+
};
|
|
403
|
+
return snapshot;
|
|
404
|
+
}
|
|
405
|
+
return {
|
|
406
|
+
get activeElement() {
|
|
407
|
+
return activeElement;
|
|
408
|
+
},
|
|
409
|
+
get activeId() {
|
|
410
|
+
return activeId;
|
|
411
|
+
},
|
|
412
|
+
get previousElement() {
|
|
413
|
+
return previousElement;
|
|
414
|
+
},
|
|
415
|
+
get previousId() {
|
|
416
|
+
return previousId;
|
|
417
|
+
},
|
|
418
|
+
get focusOrigin() {
|
|
419
|
+
return focusOrigin;
|
|
420
|
+
},
|
|
421
|
+
get scopeStack() {
|
|
422
|
+
return [...scopeStack];
|
|
423
|
+
},
|
|
424
|
+
get scopeMemory() {
|
|
425
|
+
return scopeMemory;
|
|
426
|
+
},
|
|
427
|
+
get activeScopeId() {
|
|
428
|
+
return activeScopeId;
|
|
429
|
+
},
|
|
430
|
+
focus,
|
|
431
|
+
focusById,
|
|
432
|
+
focusVirtualId,
|
|
433
|
+
blur,
|
|
434
|
+
handleSubtreeRemoved,
|
|
435
|
+
handleNodeUpdated,
|
|
436
|
+
handleSubtreeAttached,
|
|
437
|
+
enterScope,
|
|
438
|
+
exitScope,
|
|
439
|
+
activateScope,
|
|
440
|
+
getFocusPath,
|
|
441
|
+
hasFocusWithin,
|
|
442
|
+
focusNext,
|
|
443
|
+
focusPrev,
|
|
444
|
+
focusDirection,
|
|
445
|
+
subscribe,
|
|
446
|
+
getSnapshot,
|
|
447
|
+
registerHookFocusable,
|
|
448
|
+
setHookFocusableActive,
|
|
449
|
+
get hasHookFocusables() {
|
|
450
|
+
return hookFocusables.length > 0;
|
|
451
|
+
},
|
|
452
|
+
get hookFocusEnabled() {
|
|
453
|
+
return hookFocusEnabled;
|
|
454
|
+
},
|
|
455
|
+
setHookFocusEnabled
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
//#endregion
|
|
459
|
+
//#region packages/headless/src/selection.ts
|
|
460
|
+
/**
|
|
461
|
+
* Check if a character is a word character (not whitespace/punctuation).
|
|
462
|
+
* Word chars: letters, digits, underscore (matching \w).
|
|
463
|
+
*/
|
|
464
|
+
function isWordChar(ch) {
|
|
465
|
+
return /\w/.test(ch);
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Find word boundaries at a given column in the buffer row.
|
|
469
|
+
* Returns { startCol, endCol } inclusive of the word.
|
|
470
|
+
* If the position is on whitespace/punctuation, selects that single char.
|
|
471
|
+
*/
|
|
472
|
+
function findWordBoundary(buffer, col, row) {
|
|
473
|
+
const width = buffer.width;
|
|
474
|
+
const ch = buffer.getCell(col, row).char;
|
|
475
|
+
if (isWordChar(ch)) {
|
|
476
|
+
let startCol = col;
|
|
477
|
+
while (startCol > 0 && isWordChar(buffer.getCell(startCol - 1, row).char)) startCol--;
|
|
478
|
+
let endCol = col;
|
|
479
|
+
while (endCol < width - 1 && isWordChar(buffer.getCell(endCol + 1, row).char)) endCol++;
|
|
480
|
+
return {
|
|
481
|
+
startCol,
|
|
482
|
+
endCol
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
return {
|
|
486
|
+
startCol: col,
|
|
487
|
+
endCol: col
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Find line boundaries for a given row.
|
|
492
|
+
* Returns { startCol, endCol } spanning from first content to last content.
|
|
493
|
+
* If the row is empty, returns { startCol: 0, endCol: width - 1 }.
|
|
494
|
+
*/
|
|
495
|
+
function findLineBoundary(buffer, row) {
|
|
496
|
+
const width = buffer.width;
|
|
497
|
+
let endCol = width - 1;
|
|
498
|
+
while (endCol > 0 && buffer.getCell(endCol, row).char.trim() === "") endCol--;
|
|
499
|
+
let startCol = 0;
|
|
500
|
+
while (startCol < endCol && buffer.getCell(startCol, row).char.trim() === "") startCol++;
|
|
501
|
+
if (startCol >= endCol && buffer.getCell(startCol, row).char.trim() === "") return {
|
|
502
|
+
startCol: 0,
|
|
503
|
+
endCol: width - 1
|
|
504
|
+
};
|
|
505
|
+
return {
|
|
506
|
+
startCol,
|
|
507
|
+
endCol
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Extend the head position according to the current granularity.
|
|
512
|
+
* For word granularity: snaps to word boundaries.
|
|
513
|
+
* For line granularity: snaps to line boundaries.
|
|
514
|
+
*/
|
|
515
|
+
function extendByGranularity(col, row, anchor, granularity, buffer) {
|
|
516
|
+
if (granularity === "character" || !buffer) return {
|
|
517
|
+
col,
|
|
518
|
+
row
|
|
519
|
+
};
|
|
520
|
+
if (granularity === "word") {
|
|
521
|
+
const { startCol, endCol } = findWordBoundary(buffer, col, row);
|
|
522
|
+
return row > anchor.row || row === anchor.row && col >= anchor.col ? {
|
|
523
|
+
col: endCol,
|
|
524
|
+
row
|
|
525
|
+
} : {
|
|
526
|
+
col: startCol,
|
|
527
|
+
row
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
if (granularity === "line") {
|
|
531
|
+
const { startCol, endCol } = findLineBoundary(buffer, row);
|
|
532
|
+
return row > anchor.row || row === anchor.row && col >= anchor.col ? {
|
|
533
|
+
col: endCol,
|
|
534
|
+
row
|
|
535
|
+
} : {
|
|
536
|
+
col: startCol,
|
|
537
|
+
row
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
return {
|
|
541
|
+
col,
|
|
542
|
+
row
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
function createTerminalSelectionState() {
|
|
546
|
+
return {
|
|
547
|
+
range: null,
|
|
548
|
+
selecting: false,
|
|
549
|
+
source: null,
|
|
550
|
+
granularity: "character",
|
|
551
|
+
scope: null
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* Clamp a position to a scope boundary.
|
|
556
|
+
*/
|
|
557
|
+
function clampToScope(col, row, scope) {
|
|
558
|
+
if (!scope) return {
|
|
559
|
+
col,
|
|
560
|
+
row
|
|
561
|
+
};
|
|
562
|
+
return {
|
|
563
|
+
col: Math.max(scope.left, Math.min(scope.right, col)),
|
|
564
|
+
row: Math.max(scope.top, Math.min(scope.bottom, row))
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
function terminalSelectionUpdate(action, state) {
|
|
568
|
+
switch (action.type) {
|
|
569
|
+
case "start": {
|
|
570
|
+
const scope = action.scope ?? null;
|
|
571
|
+
const pos = clampToScope(action.col, action.row, scope);
|
|
572
|
+
return [{
|
|
573
|
+
range: {
|
|
574
|
+
anchor: pos,
|
|
575
|
+
head: pos
|
|
576
|
+
},
|
|
577
|
+
selecting: true,
|
|
578
|
+
source: action.source ?? "mouse",
|
|
579
|
+
granularity: "character",
|
|
580
|
+
scope
|
|
581
|
+
}, [{ type: "render" }]];
|
|
582
|
+
}
|
|
583
|
+
case "startWord": {
|
|
584
|
+
const scope = action.scope ?? null;
|
|
585
|
+
const { startCol, endCol } = findWordBoundary(action.buffer, action.col, action.row);
|
|
586
|
+
return [{
|
|
587
|
+
range: {
|
|
588
|
+
anchor: clampToScope(startCol, action.row, scope),
|
|
589
|
+
head: clampToScope(endCol, action.row, scope)
|
|
590
|
+
},
|
|
591
|
+
selecting: true,
|
|
592
|
+
source: action.source ?? "mouse",
|
|
593
|
+
granularity: "word",
|
|
594
|
+
scope
|
|
595
|
+
}, [{ type: "render" }]];
|
|
596
|
+
}
|
|
597
|
+
case "startLine": {
|
|
598
|
+
const scope = action.scope ?? null;
|
|
599
|
+
const { startCol, endCol } = findLineBoundary(action.buffer, action.row);
|
|
600
|
+
return [{
|
|
601
|
+
range: {
|
|
602
|
+
anchor: clampToScope(startCol, action.row, scope),
|
|
603
|
+
head: clampToScope(endCol, action.row, scope)
|
|
604
|
+
},
|
|
605
|
+
selecting: true,
|
|
606
|
+
source: action.source ?? "mouse",
|
|
607
|
+
granularity: "line",
|
|
608
|
+
scope
|
|
609
|
+
}, [{ type: "render" }]];
|
|
610
|
+
}
|
|
611
|
+
case "extend": {
|
|
612
|
+
if (!state.selecting) return [state, []];
|
|
613
|
+
const scope = "scope" in action ? action.scope ?? null : state.scope;
|
|
614
|
+
const extended = extendByGranularity(action.col, action.row, state.range.anchor, state.granularity, action.buffer);
|
|
615
|
+
const head = clampToScope(extended.col, extended.row, scope);
|
|
616
|
+
return [{
|
|
617
|
+
...state,
|
|
618
|
+
range: {
|
|
619
|
+
anchor: state.range.anchor,
|
|
620
|
+
head
|
|
621
|
+
},
|
|
622
|
+
scope,
|
|
623
|
+
selecting: true
|
|
624
|
+
}, [{ type: "render" }]];
|
|
625
|
+
}
|
|
626
|
+
case "finish":
|
|
627
|
+
if (!state.range) return [{
|
|
628
|
+
...state,
|
|
629
|
+
selecting: false
|
|
630
|
+
}, []];
|
|
631
|
+
return [{
|
|
632
|
+
...state,
|
|
633
|
+
range: state.range,
|
|
634
|
+
selecting: false
|
|
635
|
+
}, []];
|
|
636
|
+
case "clear": {
|
|
637
|
+
const hadRange = state.range !== null;
|
|
638
|
+
return [createTerminalSelectionState(), hadRange ? [{ type: "render" }] : []];
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
function normalizeRange(range) {
|
|
643
|
+
const { anchor, head } = range;
|
|
644
|
+
if (anchor.row < head.row || anchor.row === head.row && anchor.col <= head.col) return {
|
|
645
|
+
startRow: anchor.row,
|
|
646
|
+
startCol: anchor.col,
|
|
647
|
+
endRow: head.row,
|
|
648
|
+
endCol: head.col
|
|
649
|
+
};
|
|
650
|
+
return {
|
|
651
|
+
startRow: head.row,
|
|
652
|
+
startCol: head.col,
|
|
653
|
+
endRow: anchor.row,
|
|
654
|
+
endCol: anchor.col
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* Extract text from a buffer within a selection range.
|
|
659
|
+
*
|
|
660
|
+
* Handles:
|
|
661
|
+
* - Soft-wrap joining (via RowMetadata.softWrapped) — rejoins wrapped visual
|
|
662
|
+
* rows into their logical line, reinserting the word-wrap break space
|
|
663
|
+
* (RowMetadata.wrapJoinSpace) that trim-mode rendering stripped
|
|
664
|
+
* - Trailing fill exclusion (clamp to RowMetadata.lastContentCol; falls back
|
|
665
|
+
* to a trailing-whitespace trim when a row carries no metadata)
|
|
666
|
+
* - Blank line preservation within selection
|
|
667
|
+
* - Wide-char continuation cell skipping
|
|
668
|
+
* - SELECTABLE_FLAG filtering (when respectSelectableFlag is true)
|
|
669
|
+
* - Contain-scope clipping (when options.scope is set)
|
|
670
|
+
*/
|
|
671
|
+
function extractText(buffer, range, options) {
|
|
672
|
+
const { startRow, startCol, endRow, endCol } = normalizeRange(range);
|
|
673
|
+
const respectSelectable = options?.respectSelectableFlag ?? true;
|
|
674
|
+
const rowMeta = options?.rowMetadata;
|
|
675
|
+
const scope = options?.scope;
|
|
676
|
+
const parts = [];
|
|
677
|
+
for (let row = startRow; row <= endRow; row++) {
|
|
678
|
+
const meta = rowMeta?.[row];
|
|
679
|
+
let colStart = row === startRow ? startCol : 0;
|
|
680
|
+
let colEnd = row === endRow ? endCol : buffer.width - 1;
|
|
681
|
+
if (scope) {
|
|
682
|
+
colStart = Math.max(colStart, scope.left);
|
|
683
|
+
colEnd = Math.min(colEnd, scope.right);
|
|
684
|
+
if (colStart > colEnd) {
|
|
685
|
+
if (!(meta?.softWrapped && row < endRow)) {
|
|
686
|
+
parts.push("");
|
|
687
|
+
if (row < endRow) parts.push("\n");
|
|
688
|
+
}
|
|
689
|
+
continue;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
if (meta && meta.lastContentCol >= 0) colEnd = Math.min(colEnd, meta.lastContentCol);
|
|
693
|
+
let line = "";
|
|
694
|
+
for (let col = colStart; col <= colEnd; col++) {
|
|
695
|
+
if (buffer.isCellContinuation(col, row)) continue;
|
|
696
|
+
if (respectSelectable && !buffer.isCellSelectable(col, row)) continue;
|
|
697
|
+
line += buffer.getCellChar(col, row);
|
|
698
|
+
}
|
|
699
|
+
if (!meta || meta.lastContentCol < 0) line = line.replace(/\s+$/, "");
|
|
700
|
+
if (meta?.softWrapped && row < endRow) {
|
|
701
|
+
parts.push(line);
|
|
702
|
+
if (meta.wrapJoinSpace) parts.push(" ");
|
|
703
|
+
} else {
|
|
704
|
+
parts.push(line);
|
|
705
|
+
if (row < endRow) parts.push("\n");
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
return parts.join("");
|
|
709
|
+
}
|
|
710
|
+
//#endregion
|
|
711
|
+
//#region packages/ag-term/src/drag-events.ts
|
|
712
|
+
/**
|
|
713
|
+
* Create a DragEvent payload.
|
|
714
|
+
*/
|
|
715
|
+
function createDragEvent(source, position, dropTarget) {
|
|
716
|
+
return {
|
|
717
|
+
source,
|
|
718
|
+
position,
|
|
719
|
+
dropTarget
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* Create initial DragState when a drag starts.
|
|
724
|
+
*/
|
|
725
|
+
function createDragState(source, startPos) {
|
|
726
|
+
return {
|
|
727
|
+
active: true,
|
|
728
|
+
source,
|
|
729
|
+
startPos,
|
|
730
|
+
currentPos: startPos,
|
|
731
|
+
dropTarget: null
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* Check if a node accepts drops (has any drag event handler).
|
|
736
|
+
*/
|
|
737
|
+
function isDropTarget(node) {
|
|
738
|
+
if (!node) return false;
|
|
739
|
+
const props = node.props;
|
|
740
|
+
return !!(props.onDragEnter || props.onDragLeave || props.onDragOver || props.onDrop);
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
743
|
+
* Find the nearest ancestor (including self) that is a drop target.
|
|
744
|
+
* Returns null if no ancestor accepts drops.
|
|
745
|
+
*/
|
|
746
|
+
function findDropTarget(node) {
|
|
747
|
+
let current = node;
|
|
748
|
+
while (current) {
|
|
749
|
+
if (isDropTarget(current)) return current;
|
|
750
|
+
current = current.parent;
|
|
751
|
+
}
|
|
752
|
+
return null;
|
|
753
|
+
}
|
|
754
|
+
//#endregion
|
|
755
|
+
//#region packages/ag-react/src/hooks/useCursor.ts
|
|
756
|
+
/**
|
|
757
|
+
* useCursor - Show and position the terminal's blinking cursor.
|
|
758
|
+
*
|
|
759
|
+
* Maps component-relative (col, row) to absolute terminal coordinates
|
|
760
|
+
* using useScrollRect. Per-instance last-writer-wins: only one cursor
|
|
761
|
+
* can be active at a time per silvery instance (the terminal has one hardware cursor).
|
|
762
|
+
*
|
|
763
|
+
* Cursor state is isolated per silvery instance via CursorContext. Each runtime
|
|
764
|
+
* (run(), createApp(), test renderer) provides its own CursorProvider so
|
|
765
|
+
* multiple silvery instances don't fight over cursor position.
|
|
766
|
+
*
|
|
767
|
+
* Falls back to module-level globals when no CursorProvider is present
|
|
768
|
+
* (backward compatibility / deprecation path).
|
|
769
|
+
*/
|
|
770
|
+
/**
|
|
771
|
+
* Unique-id counter for `useCursor` instances. Module-level so the
|
|
772
|
+
* counter is shared across ALL instances in the process — guarantees
|
|
773
|
+
* uniqueness regardless of CursorProvider scope. Wraps at MAX_SAFE_INTEGER
|
|
774
|
+
* (practically infinite for terminal-UI lifetimes).
|
|
775
|
+
*/
|
|
776
|
+
let cursorInstanceIdCounter = 0;
|
|
777
|
+
/**
|
|
778
|
+
* Create an isolated cursor store. Each silvery instance gets one.
|
|
779
|
+
* Returns the store (for CursorProvider) and accessors (for scheduler/output).
|
|
780
|
+
*/
|
|
781
|
+
function createCursorStore() {
|
|
782
|
+
const store = {
|
|
783
|
+
state: null,
|
|
784
|
+
listeners: /* @__PURE__ */ new Set(),
|
|
785
|
+
accessors: null,
|
|
786
|
+
setCursorState(s) {
|
|
787
|
+
store.state = s;
|
|
788
|
+
for (const listener of store.listeners) listener();
|
|
789
|
+
}
|
|
790
|
+
};
|
|
791
|
+
store.accessors = {
|
|
792
|
+
getCursorState: () => store.state,
|
|
793
|
+
subscribeCursor: (listener) => {
|
|
794
|
+
store.listeners.add(listener);
|
|
795
|
+
return () => {
|
|
796
|
+
store.listeners.delete(listener);
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
};
|
|
800
|
+
return store;
|
|
801
|
+
}
|
|
802
|
+
const CursorCtx = createContext(null);
|
|
803
|
+
/**
|
|
804
|
+
* Provider that gives its subtree an isolated cursor store.
|
|
805
|
+
* Wrap your silvery app root in this to isolate cursor state per instance.
|
|
806
|
+
*/
|
|
807
|
+
function CursorProvider({ store, children }) {
|
|
808
|
+
return React.createElement(CursorCtx.Provider, { value: store }, children);
|
|
809
|
+
}
|
|
810
|
+
let _globalCursorState = null;
|
|
811
|
+
let _globalCursorListeners = /* @__PURE__ */ new Set();
|
|
812
|
+
function globalSetCursorState(state) {
|
|
813
|
+
_globalCursorState = state;
|
|
814
|
+
for (const listener of _globalCursorListeners) listener();
|
|
815
|
+
}
|
|
816
|
+
function globalGetCursorState() {
|
|
817
|
+
return _globalCursorState;
|
|
818
|
+
}
|
|
819
|
+
/** For testing -- reset global fallback state between tests. */
|
|
820
|
+
function resetCursorState() {
|
|
821
|
+
_globalCursorState = null;
|
|
822
|
+
_globalCursorListeners = /* @__PURE__ */ new Set();
|
|
823
|
+
}
|
|
824
|
+
/**
|
|
825
|
+
* Show and position the terminal's blinking cursor within this component.
|
|
826
|
+
*
|
|
827
|
+
* The cursor position is relative to the component's screen position.
|
|
828
|
+
* Only one cursor can be active per silvery instance -- last caller with visible=true wins.
|
|
829
|
+
*
|
|
830
|
+
* Uses CursorContext if a CursorProvider is present (per-instance isolation).
|
|
831
|
+
* Falls back to module-level globals otherwise (backward compat).
|
|
832
|
+
*
|
|
833
|
+
* @deprecated Phase 2 of `km-silvery.view-as-layout-output` migrated cursor
|
|
834
|
+
* positioning from a React-effect-chain (`useCursor` → `useScrollRect` →
|
|
835
|
+
* `setCursorState`) to a layout-output prop (`<Box cursorOffset={…}>`). The
|
|
836
|
+
* prop path resolves absolute coordinates synchronously during the layout
|
|
837
|
+
* phase, so the very first frame after a conditional mount emits correct
|
|
838
|
+
* cursor ANSI — fixing `km-silvercode.cursor-startup-position` end-to-end.
|
|
839
|
+
*
|
|
840
|
+
* Migrate consumers by deleting the `useCursor({col, row, visible})` call
|
|
841
|
+
* and adding `cursorOffset={{col, row, visible}}` to the surrounding Box. The
|
|
842
|
+
* layout phase applies border + padding offsets automatically, so consumers
|
|
843
|
+
* no longer need to add `borderColOffset` / `borderRowOffset` manually.
|
|
844
|
+
*
|
|
845
|
+
* This hook remains as a back-compat wrapper that writes to the cursor
|
|
846
|
+
* store; the scheduler still reads the store as a fallback when no
|
|
847
|
+
* `cursorOffset` prop is set on any node. Slated for deletion once all
|
|
848
|
+
* in-tree consumers migrate (TextArea + TextInput already migrated; Ink
|
|
849
|
+
* compat goes through the store directly and is unaffected). See bead
|
|
850
|
+
* `km-silvery.delete-cursor-globals`.
|
|
851
|
+
*/
|
|
852
|
+
function useCursor(position) {
|
|
853
|
+
const { col, row, visible = true, shape } = position;
|
|
854
|
+
const store = useContext(CursorCtx);
|
|
855
|
+
const node = useContext(NodeContext);
|
|
856
|
+
const set = store ? store.setCursorState.bind(store) : globalSetCursorState;
|
|
857
|
+
const get = store ? store.accessors.getCursorState : globalGetCursorState;
|
|
858
|
+
const instanceIdRef = useRef(0);
|
|
859
|
+
if (instanceIdRef.current === 0) {
|
|
860
|
+
cursorInstanceIdCounter += 1;
|
|
861
|
+
instanceIdRef.current = cursorInstanceIdCounter;
|
|
862
|
+
}
|
|
863
|
+
const props = node?.props;
|
|
864
|
+
const padLeft = props?.paddingLeft ?? props?.paddingX ?? props?.padding ?? 0;
|
|
865
|
+
const padTop = props?.paddingTop ?? props?.paddingY ?? props?.padding ?? 0;
|
|
866
|
+
const borderLeft = props?.borderStyle ? 1 : 0;
|
|
867
|
+
const borderTop = props?.borderStyle ? 1 : 0;
|
|
868
|
+
const contentOffsetX = borderLeft + padLeft;
|
|
869
|
+
const contentOffsetY = borderTop + padTop;
|
|
870
|
+
const colRef = useRef(col);
|
|
871
|
+
const rowRef = useRef(row);
|
|
872
|
+
const visibleRef = useRef(visible);
|
|
873
|
+
const shapeRef = useRef(shape);
|
|
874
|
+
const setRef = useRef(set);
|
|
875
|
+
const getRef = useRef(get);
|
|
876
|
+
const lastRectRef = useRef(null);
|
|
877
|
+
const contentOffsetXRef = useRef(contentOffsetX);
|
|
878
|
+
const contentOffsetYRef = useRef(contentOffsetY);
|
|
879
|
+
colRef.current = col;
|
|
880
|
+
rowRef.current = row;
|
|
881
|
+
visibleRef.current = visible;
|
|
882
|
+
shapeRef.current = shape;
|
|
883
|
+
setRef.current = set;
|
|
884
|
+
getRef.current = get;
|
|
885
|
+
contentOffsetXRef.current = contentOffsetX;
|
|
886
|
+
contentOffsetYRef.current = contentOffsetY;
|
|
887
|
+
const scrollRect = useScrollRect();
|
|
888
|
+
const hasNode = node != null;
|
|
889
|
+
useEffect(() => {
|
|
890
|
+
if (!hasNode) return;
|
|
891
|
+
lastRectRef.current = scrollRect;
|
|
892
|
+
if (!visibleRef.current) return;
|
|
893
|
+
setRef.current({
|
|
894
|
+
x: scrollRect.x + contentOffsetXRef.current + colRef.current,
|
|
895
|
+
y: scrollRect.y + contentOffsetYRef.current + rowRef.current,
|
|
896
|
+
visible: true,
|
|
897
|
+
shape: shapeRef.current,
|
|
898
|
+
owner: instanceIdRef.current
|
|
899
|
+
});
|
|
900
|
+
}, [
|
|
901
|
+
hasNode,
|
|
902
|
+
scrollRect.x,
|
|
903
|
+
scrollRect.y,
|
|
904
|
+
scrollRect.width,
|
|
905
|
+
scrollRect.height,
|
|
906
|
+
scrollRect
|
|
907
|
+
]);
|
|
908
|
+
const owns = () => {
|
|
909
|
+
const current = getRef.current();
|
|
910
|
+
if (!current) return false;
|
|
911
|
+
if (current.owner === void 0) return true;
|
|
912
|
+
return current.owner === instanceIdRef.current;
|
|
913
|
+
};
|
|
914
|
+
const ownsRef = useRef(owns);
|
|
915
|
+
ownsRef.current = owns;
|
|
916
|
+
useLayoutEffect(() => {
|
|
917
|
+
const rect = lastRectRef.current;
|
|
918
|
+
if (!rect) return;
|
|
919
|
+
if (visible) set({
|
|
920
|
+
x: rect.x + contentOffsetX + col,
|
|
921
|
+
y: rect.y + contentOffsetY + row,
|
|
922
|
+
visible: true,
|
|
923
|
+
shape,
|
|
924
|
+
owner: instanceIdRef.current
|
|
925
|
+
});
|
|
926
|
+
else if (ownsRef.current()) set(null);
|
|
927
|
+
}, [
|
|
928
|
+
col,
|
|
929
|
+
row,
|
|
930
|
+
contentOffsetX,
|
|
931
|
+
contentOffsetY,
|
|
932
|
+
shape,
|
|
933
|
+
visible,
|
|
934
|
+
set
|
|
935
|
+
]);
|
|
936
|
+
useEffect(() => {
|
|
937
|
+
return () => {
|
|
938
|
+
if (ownsRef.current()) setRef.current(null);
|
|
939
|
+
};
|
|
940
|
+
}, []);
|
|
941
|
+
}
|
|
942
|
+
createLogger("silvery:render");
|
|
943
|
+
createLogger("@silvery/ag-react");
|
|
944
|
+
//#endregion
|
|
945
|
+
//#region packages/ag-term/src/adapters/canvas-adapter.ts
|
|
946
|
+
const DEFAULT_CONFIG$1 = {
|
|
947
|
+
fontSize: 14,
|
|
948
|
+
fontFamily: "monospace",
|
|
949
|
+
lineHeight: 1.2,
|
|
950
|
+
backgroundColor: "#1e1e1e",
|
|
951
|
+
foregroundColor: "#d4d4d4",
|
|
952
|
+
monospace: true,
|
|
953
|
+
dpr: 1
|
|
954
|
+
};
|
|
955
|
+
const BORDER_CHARS$1 = {
|
|
956
|
+
single: {
|
|
957
|
+
topLeft: "┌",
|
|
958
|
+
topRight: "┐",
|
|
959
|
+
bottomLeft: "└",
|
|
960
|
+
bottomRight: "┘",
|
|
961
|
+
horizontal: "─",
|
|
962
|
+
vertical: "│"
|
|
963
|
+
},
|
|
964
|
+
double: {
|
|
965
|
+
topLeft: "╔",
|
|
966
|
+
topRight: "╗",
|
|
967
|
+
bottomLeft: "╚",
|
|
968
|
+
bottomRight: "╝",
|
|
969
|
+
horizontal: "═",
|
|
970
|
+
vertical: "║"
|
|
971
|
+
},
|
|
972
|
+
round: {
|
|
973
|
+
topLeft: "╭",
|
|
974
|
+
topRight: "╮",
|
|
975
|
+
bottomLeft: "╰",
|
|
976
|
+
bottomRight: "╯",
|
|
977
|
+
horizontal: "─",
|
|
978
|
+
vertical: "│"
|
|
979
|
+
},
|
|
980
|
+
bold: {
|
|
981
|
+
topLeft: "┏",
|
|
982
|
+
topRight: "┓",
|
|
983
|
+
bottomLeft: "┗",
|
|
984
|
+
bottomRight: "┛",
|
|
985
|
+
horizontal: "━",
|
|
986
|
+
vertical: "┃"
|
|
987
|
+
}
|
|
988
|
+
};
|
|
989
|
+
/** Create a scratch canvas 2D context for text measurement. */
|
|
990
|
+
function createMeasureContext(fontSize, fontFamily) {
|
|
991
|
+
const canvas = typeof document !== "undefined" ? document.createElement("canvas") : typeof OffscreenCanvas !== "undefined" ? new OffscreenCanvas(1, 1) : null;
|
|
992
|
+
if (!canvas) throw new Error("Canvas not available for text measurement");
|
|
993
|
+
const ctx = canvas.getContext("2d");
|
|
994
|
+
if (!ctx) throw new Error("Could not get 2d context for measurement");
|
|
995
|
+
ctx.font = `${fontSize}px ${fontFamily}`;
|
|
996
|
+
return ctx;
|
|
997
|
+
}
|
|
998
|
+
function createCanvasMeasurer(config) {
|
|
999
|
+
if (config.monospace) return {
|
|
1000
|
+
measureText(text, _style) {
|
|
1001
|
+
return {
|
|
1002
|
+
width: text.length,
|
|
1003
|
+
height: 1
|
|
1004
|
+
};
|
|
1005
|
+
},
|
|
1006
|
+
getLineHeight(_style) {
|
|
1007
|
+
return 1;
|
|
1008
|
+
}
|
|
1009
|
+
};
|
|
1010
|
+
const lineHeightPx = config.fontSize * config.lineHeight;
|
|
1011
|
+
const ctx = createMeasureContext(config.fontSize, config.fontFamily);
|
|
1012
|
+
return {
|
|
1013
|
+
measureText(text, _style) {
|
|
1014
|
+
return {
|
|
1015
|
+
width: ctx.measureText(text).width,
|
|
1016
|
+
height: lineHeightPx
|
|
1017
|
+
};
|
|
1018
|
+
},
|
|
1019
|
+
getLineHeight(_style) {
|
|
1020
|
+
return lineHeightPx;
|
|
1021
|
+
}
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
const ANSI_COLORS$1 = {
|
|
1025
|
+
black: "#000000",
|
|
1026
|
+
red: "#cd0000",
|
|
1027
|
+
green: "#00cd00",
|
|
1028
|
+
yellow: "#cdcd00",
|
|
1029
|
+
blue: "#0000ee",
|
|
1030
|
+
magenta: "#cd00cd",
|
|
1031
|
+
cyan: "#00cdcd",
|
|
1032
|
+
white: "#e5e5e5",
|
|
1033
|
+
gray: "#7f7f7f",
|
|
1034
|
+
grey: "#7f7f7f",
|
|
1035
|
+
brightBlack: "#7f7f7f",
|
|
1036
|
+
brightRed: "#ff0000",
|
|
1037
|
+
brightGreen: "#00ff00",
|
|
1038
|
+
brightYellow: "#ffff00",
|
|
1039
|
+
brightBlue: "#5c5cff",
|
|
1040
|
+
brightMagenta: "#ff00ff",
|
|
1041
|
+
brightCyan: "#00ffff",
|
|
1042
|
+
brightWhite: "#ffffff"
|
|
1043
|
+
};
|
|
1044
|
+
function resolveColor$1(color, fallback) {
|
|
1045
|
+
if (!color) return fallback;
|
|
1046
|
+
if (color.startsWith("#") || color.startsWith("rgb")) return color;
|
|
1047
|
+
const named = ANSI_COLORS$1[color.toLowerCase()];
|
|
1048
|
+
if (named) return named;
|
|
1049
|
+
return color;
|
|
1050
|
+
}
|
|
1051
|
+
var CanvasRenderBuffer = class {
|
|
1052
|
+
width;
|
|
1053
|
+
height;
|
|
1054
|
+
canvas;
|
|
1055
|
+
ctx;
|
|
1056
|
+
config;
|
|
1057
|
+
charWidth;
|
|
1058
|
+
cellHeight;
|
|
1059
|
+
constructor(width, height, config) {
|
|
1060
|
+
this.width = width;
|
|
1061
|
+
this.height = height;
|
|
1062
|
+
this.config = config;
|
|
1063
|
+
const dpr = config.dpr;
|
|
1064
|
+
let cssWidth;
|
|
1065
|
+
let cssHeight;
|
|
1066
|
+
if (config.monospace) {
|
|
1067
|
+
this.charWidth = config.fontSize * .6;
|
|
1068
|
+
this.cellHeight = config.fontSize * config.lineHeight;
|
|
1069
|
+
cssWidth = width * this.charWidth;
|
|
1070
|
+
cssHeight = height * this.cellHeight;
|
|
1071
|
+
} else {
|
|
1072
|
+
this.charWidth = 1;
|
|
1073
|
+
this.cellHeight = 1;
|
|
1074
|
+
cssWidth = width;
|
|
1075
|
+
cssHeight = height;
|
|
1076
|
+
}
|
|
1077
|
+
const nativeWidth = Math.ceil(cssWidth * dpr);
|
|
1078
|
+
const nativeHeight = Math.ceil(cssHeight * dpr);
|
|
1079
|
+
if (typeof OffscreenCanvas !== "undefined") this.canvas = new OffscreenCanvas(nativeWidth, nativeHeight);
|
|
1080
|
+
else if (typeof document !== "undefined") {
|
|
1081
|
+
this.canvas = document.createElement("canvas");
|
|
1082
|
+
this.canvas.width = nativeWidth;
|
|
1083
|
+
this.canvas.height = nativeHeight;
|
|
1084
|
+
} else throw new Error("Canvas not available");
|
|
1085
|
+
const ctx = this.canvas.getContext("2d");
|
|
1086
|
+
if (!ctx) throw new Error("Could not get 2d context");
|
|
1087
|
+
this.ctx = ctx;
|
|
1088
|
+
if (dpr !== 1) this.ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
1089
|
+
this.ctx.fillStyle = config.backgroundColor;
|
|
1090
|
+
this.ctx.fillRect(0, 0, cssWidth, cssHeight);
|
|
1091
|
+
}
|
|
1092
|
+
fillRect(x, y, width, height, style) {
|
|
1093
|
+
if (style.bg) {
|
|
1094
|
+
const px = x * this.charWidth;
|
|
1095
|
+
const py = y * this.cellHeight;
|
|
1096
|
+
const pw = width * this.charWidth;
|
|
1097
|
+
const ph = height * this.cellHeight;
|
|
1098
|
+
this.ctx.fillStyle = resolveColor$1(style.bg, this.config.backgroundColor);
|
|
1099
|
+
this.ctx.fillRect(px, py, pw, ph);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
drawText(x, y, text, style) {
|
|
1103
|
+
const px = x * this.charWidth;
|
|
1104
|
+
const attrs = style.attrs ?? {};
|
|
1105
|
+
const weight = attrs.bold ? "bold" : "normal";
|
|
1106
|
+
const fontStyle = attrs.italic ? "italic" : "normal";
|
|
1107
|
+
this.ctx.font = `${fontStyle} ${weight} ${this.config.fontSize}px ${this.config.fontFamily}`;
|
|
1108
|
+
let py = y * this.cellHeight;
|
|
1109
|
+
this.ctx.fillStyle = resolveColor$1(style.fg, this.config.foregroundColor);
|
|
1110
|
+
if (!this.config.monospace) {
|
|
1111
|
+
const metrics = this.ctx.measureText("Hg");
|
|
1112
|
+
const fontAscent = metrics.fontBoundingBoxAscent;
|
|
1113
|
+
const contentArea = fontAscent + metrics.fontBoundingBoxDescent;
|
|
1114
|
+
const halfLeading = (this.config.fontSize * this.config.lineHeight - contentArea) / 2;
|
|
1115
|
+
this.ctx.textBaseline = "alphabetic";
|
|
1116
|
+
py += halfLeading + fontAscent;
|
|
1117
|
+
} else this.ctx.textBaseline = "top";
|
|
1118
|
+
this.ctx.fillText(text, px, py);
|
|
1119
|
+
if (attrs.underline) this.drawUnderline(px, py, text, style);
|
|
1120
|
+
if (attrs.strikethrough) {
|
|
1121
|
+
const textWidth = this.ctx.measureText(text).width;
|
|
1122
|
+
const strikeY = py + this.config.fontSize * .5;
|
|
1123
|
+
this.ctx.strokeStyle = resolveColor$1(style.fg, this.config.foregroundColor);
|
|
1124
|
+
this.ctx.lineWidth = 1;
|
|
1125
|
+
this.ctx.beginPath();
|
|
1126
|
+
this.ctx.moveTo(px, strikeY);
|
|
1127
|
+
this.ctx.lineTo(px + textWidth, strikeY);
|
|
1128
|
+
this.ctx.stroke();
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
/**
|
|
1132
|
+
* Draw underline decorations at pixel coordinates.
|
|
1133
|
+
* Note: px, py are already in pixel coordinates.
|
|
1134
|
+
*/
|
|
1135
|
+
drawUnderline(px, py, text, style) {
|
|
1136
|
+
const attrs = style.attrs ?? {};
|
|
1137
|
+
const textWidth = this.ctx.measureText(text).width;
|
|
1138
|
+
const underlineY = py + this.config.fontSize * .9;
|
|
1139
|
+
const underlineColor = resolveColor$1(attrs.underlineColor ?? style.fg, this.config.foregroundColor);
|
|
1140
|
+
this.ctx.strokeStyle = underlineColor;
|
|
1141
|
+
this.ctx.lineWidth = 1;
|
|
1142
|
+
switch (attrs.underlineStyle ?? "single") {
|
|
1143
|
+
case "double":
|
|
1144
|
+
this.ctx.beginPath();
|
|
1145
|
+
this.ctx.moveTo(px, underlineY - 1);
|
|
1146
|
+
this.ctx.lineTo(px + textWidth, underlineY - 1);
|
|
1147
|
+
this.ctx.moveTo(px, underlineY + 1);
|
|
1148
|
+
this.ctx.lineTo(px + textWidth, underlineY + 1);
|
|
1149
|
+
this.ctx.stroke();
|
|
1150
|
+
break;
|
|
1151
|
+
case "curly":
|
|
1152
|
+
this.ctx.beginPath();
|
|
1153
|
+
this.ctx.moveTo(px, underlineY);
|
|
1154
|
+
const waveLength = 4;
|
|
1155
|
+
const amplitude = 2;
|
|
1156
|
+
for (let wx = 0; wx < textWidth; wx += waveLength * 2) {
|
|
1157
|
+
this.ctx.quadraticCurveTo(px + wx + waveLength / 2, underlineY - amplitude, px + wx + waveLength, underlineY);
|
|
1158
|
+
this.ctx.quadraticCurveTo(px + wx + waveLength * 3 / 2, underlineY + amplitude, px + wx + waveLength * 2, underlineY);
|
|
1159
|
+
}
|
|
1160
|
+
this.ctx.stroke();
|
|
1161
|
+
break;
|
|
1162
|
+
case "dotted":
|
|
1163
|
+
this.ctx.setLineDash([2, 2]);
|
|
1164
|
+
this.ctx.beginPath();
|
|
1165
|
+
this.ctx.moveTo(px, underlineY);
|
|
1166
|
+
this.ctx.lineTo(px + textWidth, underlineY);
|
|
1167
|
+
this.ctx.stroke();
|
|
1168
|
+
this.ctx.setLineDash([]);
|
|
1169
|
+
break;
|
|
1170
|
+
case "dashed":
|
|
1171
|
+
this.ctx.setLineDash([4, 2]);
|
|
1172
|
+
this.ctx.beginPath();
|
|
1173
|
+
this.ctx.moveTo(px, underlineY);
|
|
1174
|
+
this.ctx.lineTo(px + textWidth, underlineY);
|
|
1175
|
+
this.ctx.stroke();
|
|
1176
|
+
this.ctx.setLineDash([]);
|
|
1177
|
+
break;
|
|
1178
|
+
default:
|
|
1179
|
+
this.ctx.beginPath();
|
|
1180
|
+
this.ctx.moveTo(px, underlineY);
|
|
1181
|
+
this.ctx.lineTo(px + textWidth, underlineY);
|
|
1182
|
+
this.ctx.stroke();
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
drawChar(x, y, char, style) {
|
|
1186
|
+
this.drawText(x, y, char, style);
|
|
1187
|
+
}
|
|
1188
|
+
inBounds(x, y) {
|
|
1189
|
+
return x >= 0 && x < this.width && y >= 0 && y < this.height;
|
|
1190
|
+
}
|
|
1191
|
+
fillRoundedRect(x, y, width, height, radius, fill, stroke, lineWidth = 1) {
|
|
1192
|
+
const px = x * this.charWidth;
|
|
1193
|
+
const py = y * this.cellHeight;
|
|
1194
|
+
const pw = width * this.charWidth;
|
|
1195
|
+
const ph = height * this.cellHeight;
|
|
1196
|
+
const r = Math.min(radius, pw / 2, ph / 2);
|
|
1197
|
+
this.ctx.beginPath();
|
|
1198
|
+
this.ctx.moveTo(px + r, py);
|
|
1199
|
+
this.ctx.lineTo(px + pw - r, py);
|
|
1200
|
+
this.ctx.quadraticCurveTo(px + pw, py, px + pw, py + r);
|
|
1201
|
+
this.ctx.lineTo(px + pw, py + ph - r);
|
|
1202
|
+
this.ctx.quadraticCurveTo(px + pw, py + ph, px + pw - r, py + ph);
|
|
1203
|
+
this.ctx.lineTo(px + r, py + ph);
|
|
1204
|
+
this.ctx.quadraticCurveTo(px, py + ph, px, py + ph - r);
|
|
1205
|
+
this.ctx.lineTo(px, py + r);
|
|
1206
|
+
this.ctx.quadraticCurveTo(px, py, px + r, py);
|
|
1207
|
+
this.ctx.closePath();
|
|
1208
|
+
if (fill) {
|
|
1209
|
+
this.ctx.fillStyle = fill;
|
|
1210
|
+
this.ctx.fill();
|
|
1211
|
+
}
|
|
1212
|
+
if (stroke) {
|
|
1213
|
+
this.ctx.strokeStyle = stroke;
|
|
1214
|
+
this.ctx.lineWidth = lineWidth;
|
|
1215
|
+
this.ctx.stroke();
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
};
|
|
1219
|
+
function createCanvasAdapter(config = {}) {
|
|
1220
|
+
const cfg = {
|
|
1221
|
+
...DEFAULT_CONFIG$1,
|
|
1222
|
+
...config
|
|
1223
|
+
};
|
|
1224
|
+
return {
|
|
1225
|
+
name: "canvas",
|
|
1226
|
+
measurer: createCanvasMeasurer(cfg),
|
|
1227
|
+
createBuffer(width, height) {
|
|
1228
|
+
return new CanvasRenderBuffer(width, height, cfg);
|
|
1229
|
+
},
|
|
1230
|
+
flush(_buffer, _prevBuffer) {},
|
|
1231
|
+
getBorderChars(style) {
|
|
1232
|
+
return BORDER_CHARS$1[style] ?? BORDER_CHARS$1.single;
|
|
1233
|
+
}
|
|
1234
|
+
};
|
|
1235
|
+
}
|
|
1236
|
+
//#endregion
|
|
1237
|
+
//#region packages/ag-term/src/adapters/dom-adapter.ts
|
|
1238
|
+
const DEFAULT_CONFIG = {
|
|
1239
|
+
fontSize: 14,
|
|
1240
|
+
fontFamily: "monospace",
|
|
1241
|
+
lineHeight: 1.2,
|
|
1242
|
+
backgroundColor: "#1e1e1e",
|
|
1243
|
+
foregroundColor: "#d4d4d4",
|
|
1244
|
+
classPrefix: "silvery"
|
|
1245
|
+
};
|
|
1246
|
+
const BORDER_CHARS = {
|
|
1247
|
+
single: {
|
|
1248
|
+
topLeft: "┌",
|
|
1249
|
+
topRight: "┐",
|
|
1250
|
+
bottomLeft: "└",
|
|
1251
|
+
bottomRight: "┘",
|
|
1252
|
+
horizontal: "─",
|
|
1253
|
+
vertical: "│"
|
|
1254
|
+
},
|
|
1255
|
+
double: {
|
|
1256
|
+
topLeft: "╔",
|
|
1257
|
+
topRight: "╗",
|
|
1258
|
+
bottomLeft: "╚",
|
|
1259
|
+
bottomRight: "╝",
|
|
1260
|
+
horizontal: "═",
|
|
1261
|
+
vertical: "║"
|
|
1262
|
+
},
|
|
1263
|
+
round: {
|
|
1264
|
+
topLeft: "╭",
|
|
1265
|
+
topRight: "╮",
|
|
1266
|
+
bottomLeft: "╰",
|
|
1267
|
+
bottomRight: "╯",
|
|
1268
|
+
horizontal: "─",
|
|
1269
|
+
vertical: "│"
|
|
1270
|
+
},
|
|
1271
|
+
bold: {
|
|
1272
|
+
topLeft: "┏",
|
|
1273
|
+
topRight: "┓",
|
|
1274
|
+
bottomLeft: "┗",
|
|
1275
|
+
bottomRight: "┛",
|
|
1276
|
+
horizontal: "━",
|
|
1277
|
+
vertical: "┃"
|
|
1278
|
+
}
|
|
1279
|
+
};
|
|
1280
|
+
const ANSI_COLORS = {
|
|
1281
|
+
black: "#000000",
|
|
1282
|
+
red: "#cd0000",
|
|
1283
|
+
green: "#00cd00",
|
|
1284
|
+
yellow: "#cdcd00",
|
|
1285
|
+
blue: "#0000ee",
|
|
1286
|
+
magenta: "#cd00cd",
|
|
1287
|
+
cyan: "#00cdcd",
|
|
1288
|
+
white: "#e5e5e5",
|
|
1289
|
+
gray: "#7f7f7f",
|
|
1290
|
+
grey: "#7f7f7f",
|
|
1291
|
+
brightblack: "#7f7f7f",
|
|
1292
|
+
brightred: "#ff0000",
|
|
1293
|
+
brightgreen: "#00ff00",
|
|
1294
|
+
brightyellow: "#ffff00",
|
|
1295
|
+
brightblue: "#5c5cff",
|
|
1296
|
+
brightmagenta: "#ff00ff",
|
|
1297
|
+
brightcyan: "#00ffff",
|
|
1298
|
+
brightwhite: "#ffffff"
|
|
1299
|
+
};
|
|
1300
|
+
function resolveColor(color, fallback) {
|
|
1301
|
+
if (!color) return fallback;
|
|
1302
|
+
if (color.startsWith("#") || color.startsWith("rgb")) return color;
|
|
1303
|
+
return ANSI_COLORS[color.toLowerCase()] ?? color;
|
|
1304
|
+
}
|
|
1305
|
+
function createDOMMeasurer(_config) {
|
|
1306
|
+
return {
|
|
1307
|
+
measureText(text, _style) {
|
|
1308
|
+
return {
|
|
1309
|
+
width: text.length,
|
|
1310
|
+
height: 1
|
|
1311
|
+
};
|
|
1312
|
+
},
|
|
1313
|
+
getLineHeight(_style) {
|
|
1314
|
+
return 1;
|
|
1315
|
+
}
|
|
1316
|
+
};
|
|
1317
|
+
}
|
|
1318
|
+
var DOMRenderBuffer = class {
|
|
1319
|
+
width;
|
|
1320
|
+
height;
|
|
1321
|
+
config;
|
|
1322
|
+
lines;
|
|
1323
|
+
backgrounds;
|
|
1324
|
+
charWidth;
|
|
1325
|
+
cellHeight;
|
|
1326
|
+
container = null;
|
|
1327
|
+
constructor(width, height, config) {
|
|
1328
|
+
this.width = width;
|
|
1329
|
+
this.height = height;
|
|
1330
|
+
this.config = config;
|
|
1331
|
+
this.lines = /* @__PURE__ */ new Map();
|
|
1332
|
+
this.backgrounds = /* @__PURE__ */ new Map();
|
|
1333
|
+
this.charWidth = config.fontSize * .6;
|
|
1334
|
+
this.cellHeight = config.fontSize * config.lineHeight;
|
|
1335
|
+
}
|
|
1336
|
+
/**
|
|
1337
|
+
* Set the container element for rendering.
|
|
1338
|
+
*/
|
|
1339
|
+
setContainer(container) {
|
|
1340
|
+
this.container = container;
|
|
1341
|
+
}
|
|
1342
|
+
/**
|
|
1343
|
+
* Get the container element.
|
|
1344
|
+
*/
|
|
1345
|
+
getContainer() {
|
|
1346
|
+
return this.container;
|
|
1347
|
+
}
|
|
1348
|
+
fillRect(x, y, width, height, style) {
|
|
1349
|
+
if (style.bg) {
|
|
1350
|
+
const key = `${x},${y},${width},${height}`;
|
|
1351
|
+
this.backgrounds.set(key, {
|
|
1352
|
+
x,
|
|
1353
|
+
y,
|
|
1354
|
+
w: width,
|
|
1355
|
+
h: height,
|
|
1356
|
+
color: resolveColor(style.bg, this.config.backgroundColor)
|
|
1357
|
+
});
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
drawText(x, y, text, style) {
|
|
1361
|
+
if (!this.lines.has(y)) this.lines.set(y, []);
|
|
1362
|
+
this.lines.get(y).push({
|
|
1363
|
+
text,
|
|
1364
|
+
style,
|
|
1365
|
+
x
|
|
1366
|
+
});
|
|
1367
|
+
}
|
|
1368
|
+
drawChar(x, y, char, style) {
|
|
1369
|
+
this.drawText(x, y, char, style);
|
|
1370
|
+
}
|
|
1371
|
+
inBounds(x, y) {
|
|
1372
|
+
return x >= 0 && x < this.width && y >= 0 && y < this.height;
|
|
1373
|
+
}
|
|
1374
|
+
/**
|
|
1375
|
+
* Render the buffer to the container element.
|
|
1376
|
+
* Coordinates in the buffer are in cell units (cols/rows).
|
|
1377
|
+
* This method converts them to pixel coordinates for DOM positioning.
|
|
1378
|
+
*/
|
|
1379
|
+
render() {
|
|
1380
|
+
if (!this.container) throw new Error("DOMRenderBuffer: No container set. Call setContainer() first.");
|
|
1381
|
+
const container = this.container;
|
|
1382
|
+
const cw = this.charWidth;
|
|
1383
|
+
const ch = this.cellHeight;
|
|
1384
|
+
const containerWidthPx = this.width * cw;
|
|
1385
|
+
const containerHeightPx = this.height * ch;
|
|
1386
|
+
container.innerHTML = "";
|
|
1387
|
+
container.style.cssText = `
|
|
1388
|
+
position: relative;
|
|
1389
|
+
font-family: ${this.config.fontFamily};
|
|
1390
|
+
font-size: ${this.config.fontSize}px;
|
|
1391
|
+
line-height: ${this.config.lineHeight};
|
|
1392
|
+
background-color: ${this.config.backgroundColor};
|
|
1393
|
+
color: ${this.config.foregroundColor};
|
|
1394
|
+
white-space: pre;
|
|
1395
|
+
overflow: hidden;
|
|
1396
|
+
width: ${containerWidthPx}px;
|
|
1397
|
+
height: ${containerHeightPx}px;
|
|
1398
|
+
`;
|
|
1399
|
+
for (const bg of this.backgrounds.values()) {
|
|
1400
|
+
const bgDiv = document.createElement("div");
|
|
1401
|
+
bgDiv.className = `${this.config.classPrefix}-bg`;
|
|
1402
|
+
bgDiv.style.cssText = `
|
|
1403
|
+
position: absolute;
|
|
1404
|
+
left: ${bg.x * cw}px;
|
|
1405
|
+
top: ${bg.y * ch}px;
|
|
1406
|
+
width: ${bg.w * cw}px;
|
|
1407
|
+
height: ${bg.h * ch}px;
|
|
1408
|
+
background-color: ${bg.color};
|
|
1409
|
+
`;
|
|
1410
|
+
container.appendChild(bgDiv);
|
|
1411
|
+
}
|
|
1412
|
+
const sortedLines = Array.from(this.lines.entries()).sort((a, b) => a[0] - b[0]);
|
|
1413
|
+
for (const [y, runs] of sortedLines) {
|
|
1414
|
+
const lineDiv = document.createElement("div");
|
|
1415
|
+
lineDiv.className = `${this.config.classPrefix}-line`;
|
|
1416
|
+
lineDiv.style.cssText = `
|
|
1417
|
+
position: absolute;
|
|
1418
|
+
left: 0;
|
|
1419
|
+
top: ${y * ch}px;
|
|
1420
|
+
height: ${ch}px;
|
|
1421
|
+
white-space: pre;
|
|
1422
|
+
`;
|
|
1423
|
+
const sortedRuns = runs.sort((a, b) => a.x - b.x);
|
|
1424
|
+
for (const run of sortedRuns) {
|
|
1425
|
+
const span = document.createElement(run.style.hyperlink === void 0 ? "span" : "a");
|
|
1426
|
+
span.className = `${this.config.classPrefix}-text`;
|
|
1427
|
+
span.textContent = run.text;
|
|
1428
|
+
if (run.style.hyperlink !== void 0) span.setAttribute("href", run.style.hyperlink);
|
|
1429
|
+
const styles = [`position: absolute`, `left: ${run.x * cw}px`];
|
|
1430
|
+
if (run.style.fg) styles.push(`color: ${resolveColor(run.style.fg, this.config.foregroundColor)}`);
|
|
1431
|
+
if (run.style.bg) styles.push(`background-color: ${resolveColor(run.style.bg, this.config.backgroundColor)}`);
|
|
1432
|
+
const attrs = run.style.attrs;
|
|
1433
|
+
if (attrs) {
|
|
1434
|
+
if (attrs.bold) styles.push("font-weight: bold");
|
|
1435
|
+
if (attrs.dim) styles.push("opacity: 0.5");
|
|
1436
|
+
if (attrs.italic) styles.push("font-style: italic");
|
|
1437
|
+
if (attrs.underline || attrs.underlineStyle) {
|
|
1438
|
+
const underlineStyle = attrs.underlineStyle ?? "single";
|
|
1439
|
+
const underlineColor = attrs.underlineColor ? resolveColor(attrs.underlineColor, this.config.foregroundColor) : "currentColor";
|
|
1440
|
+
switch (underlineStyle) {
|
|
1441
|
+
case "double":
|
|
1442
|
+
styles.push(`text-decoration: underline double ${underlineColor}`);
|
|
1443
|
+
break;
|
|
1444
|
+
case "curly":
|
|
1445
|
+
styles.push(`text-decoration: underline wavy ${underlineColor}`);
|
|
1446
|
+
break;
|
|
1447
|
+
case "dotted":
|
|
1448
|
+
styles.push(`text-decoration: underline dotted ${underlineColor}`);
|
|
1449
|
+
break;
|
|
1450
|
+
case "dashed":
|
|
1451
|
+
styles.push(`text-decoration: underline dashed ${underlineColor}`);
|
|
1452
|
+
break;
|
|
1453
|
+
default: styles.push(`text-decoration: underline solid ${underlineColor}`);
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
if (attrs.strikethrough) {
|
|
1457
|
+
const existing = styles.find((s) => s.startsWith("text-decoration:"));
|
|
1458
|
+
if (existing) {
|
|
1459
|
+
const idx = styles.indexOf(existing);
|
|
1460
|
+
styles[idx] = existing.replace("underline", "underline line-through");
|
|
1461
|
+
} else styles.push("text-decoration: line-through");
|
|
1462
|
+
}
|
|
1463
|
+
if (attrs.inverse) {
|
|
1464
|
+
const fg = run.style.fg ? resolveColor(run.style.fg, this.config.foregroundColor) : this.config.foregroundColor;
|
|
1465
|
+
const bg = run.style.bg ? resolveColor(run.style.bg, this.config.backgroundColor) : this.config.backgroundColor;
|
|
1466
|
+
styles.push(`color: ${bg}`, `background-color: ${fg}`);
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
span.style.cssText = styles.join("; ");
|
|
1470
|
+
lineDiv.appendChild(span);
|
|
1471
|
+
}
|
|
1472
|
+
container.appendChild(lineDiv);
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
/**
|
|
1476
|
+
* Clear the buffer.
|
|
1477
|
+
*/
|
|
1478
|
+
clear() {
|
|
1479
|
+
this.lines.clear();
|
|
1480
|
+
this.backgrounds.clear();
|
|
1481
|
+
}
|
|
1482
|
+
};
|
|
1483
|
+
function createDOMAdapter(config = {}) {
|
|
1484
|
+
const cfg = {
|
|
1485
|
+
...DEFAULT_CONFIG,
|
|
1486
|
+
...config
|
|
1487
|
+
};
|
|
1488
|
+
return {
|
|
1489
|
+
name: "dom",
|
|
1490
|
+
measurer: createDOMMeasurer(cfg),
|
|
1491
|
+
createBuffer(width, height) {
|
|
1492
|
+
return new DOMRenderBuffer(width, height, cfg);
|
|
1493
|
+
},
|
|
1494
|
+
flush(buffer, _prevBuffer) {
|
|
1495
|
+
const domBuffer = buffer;
|
|
1496
|
+
if (domBuffer.getContainer()) domBuffer.render();
|
|
1497
|
+
},
|
|
1498
|
+
getBorderChars(style) {
|
|
1499
|
+
return BORDER_CHARS[style] ?? BORDER_CHARS.single;
|
|
1500
|
+
}
|
|
1501
|
+
};
|
|
1502
|
+
}
|
|
1503
|
+
let stylesInjected = false;
|
|
1504
|
+
/**
|
|
1505
|
+
* Inject global CSS styles for silvery DOM rendering.
|
|
1506
|
+
* Call once at application startup if you want default styling.
|
|
1507
|
+
*/
|
|
1508
|
+
function injectDOMStyles(classPrefix = "silvery") {
|
|
1509
|
+
if (stylesInjected || typeof document === "undefined") return;
|
|
1510
|
+
const style = document.createElement("style");
|
|
1511
|
+
style.textContent = `
|
|
1512
|
+
.${classPrefix}-container {
|
|
1513
|
+
font-family: monospace;
|
|
1514
|
+
white-space: pre;
|
|
1515
|
+
overflow: hidden;
|
|
1516
|
+
}
|
|
1517
|
+
.${classPrefix}-line {
|
|
1518
|
+
white-space: pre;
|
|
1519
|
+
}
|
|
1520
|
+
.${classPrefix}-text {
|
|
1521
|
+
white-space: pre;
|
|
1522
|
+
}
|
|
1523
|
+
/* Selection styling */
|
|
1524
|
+
.${classPrefix}-text::selection {
|
|
1525
|
+
background-color: rgba(100, 150, 255, 0.3);
|
|
1526
|
+
}
|
|
1527
|
+
`;
|
|
1528
|
+
document.head.appendChild(style);
|
|
1529
|
+
stylesInjected = true;
|
|
1530
|
+
}
|
|
1531
|
+
//#endregion
|
|
1532
|
+
//#region packages/ag-term/src/caret-style.ts
|
|
1533
|
+
/**
|
|
1534
|
+
* Walk a tree and return the AgNode whose `cursorOffset` produced the active
|
|
1535
|
+
* caret rect (matching the precedence rules of `findActiveCursorRect`).
|
|
1536
|
+
*
|
|
1537
|
+
* Used by the scheduler to feed `resolveCaretStyle`. Re-walks the tree to
|
|
1538
|
+
* keep `findActiveCursorRect` returning a value-only (data) result rather
|
|
1539
|
+
* than coupling it to the AgNode reference.
|
|
1540
|
+
*
|
|
1541
|
+
* Returns null when no caret is active.
|
|
1542
|
+
*/
|
|
1543
|
+
function findActiveCursorNode(root) {
|
|
1544
|
+
let focusedNode = null;
|
|
1545
|
+
let focusedSuppressesFallback = false;
|
|
1546
|
+
let fallbackNode = null;
|
|
1547
|
+
function walk(node) {
|
|
1548
|
+
for (const child of node.children) walk(child);
|
|
1549
|
+
const props = node.props;
|
|
1550
|
+
if (!props?.cursorOffset) return;
|
|
1551
|
+
const offset = props.cursorOffset;
|
|
1552
|
+
if (props.focused === true || node.interactiveState?.focused) {
|
|
1553
|
+
focusedSuppressesFallback = true;
|
|
1554
|
+
focusedNode = node;
|
|
1555
|
+
return;
|
|
1556
|
+
}
|
|
1557
|
+
if (offset.visible !== false) fallbackNode = node;
|
|
1558
|
+
}
|
|
1559
|
+
walk(root);
|
|
1560
|
+
return focusedNode ?? (focusedSuppressesFallback ? null : fallbackNode);
|
|
1561
|
+
}
|
|
1562
|
+
//#endregion
|
|
1563
|
+
//#region packages/ag-term/src/osc-markers.ts
|
|
1564
|
+
/**
|
|
1565
|
+
* OSC 133 Semantic Prompt Markers
|
|
1566
|
+
*
|
|
1567
|
+
* Shell integration protocol that marks prompts and commands in terminal output.
|
|
1568
|
+
* Terminals like iTerm2, Kitty, and WezTerm use these markers to provide
|
|
1569
|
+
* "jump to previous/next prompt" navigation (Cmd+Up/Cmd+Down in iTerm2).
|
|
1570
|
+
*
|
|
1571
|
+
* Protocol: OSC 133
|
|
1572
|
+
* - Prompt start: ESC ] 133 ; A BEL (before user input)
|
|
1573
|
+
* - Prompt end: ESC ] 133 ; B BEL (after user input, before command output)
|
|
1574
|
+
* - Command output start: ESC ] 133 ; C BEL (before command output)
|
|
1575
|
+
* - Command output end: ESC ] 133 ; D ; N BEL (after command output, N = exit code)
|
|
1576
|
+
*
|
|
1577
|
+
* For a chat-style app, each "exchange" (user prompt + assistant response) maps to:
|
|
1578
|
+
* - 133;A before the user's message
|
|
1579
|
+
* - 133;B after the user's message
|
|
1580
|
+
* - 133;C before the assistant's response
|
|
1581
|
+
* - 133;D;0 after the assistant's response
|
|
1582
|
+
*
|
|
1583
|
+
* Supported by: iTerm2, Kitty, WezTerm, foot, Ghostty
|
|
1584
|
+
*/
|
|
1585
|
+
const OSC133 = {
|
|
1586
|
+
promptStart: "\x1B]133;A\x07",
|
|
1587
|
+
promptEnd: "\x1B]133;B\x07",
|
|
1588
|
+
commandStart: "\x1B]133;C\x07",
|
|
1589
|
+
commandEnd: (exitCode) => `\x1b]133;D;${exitCode ?? 0}\x07`
|
|
1590
|
+
};
|
|
1591
|
+
//#endregion
|
|
1592
|
+
//#region packages/ag-term/src/kitty-detect.ts
|
|
1593
|
+
/** Regex to match a Kitty keyboard query response: CSI ? <flags> u */
|
|
1594
|
+
const KITTY_RESPONSE_RE = /\x1b\[\?(\d+)u/;
|
|
1595
|
+
/**
|
|
1596
|
+
* Detect Kitty keyboard protocol support.
|
|
1597
|
+
*
|
|
1598
|
+
* Sends CSI ? u to the terminal and waits for a response.
|
|
1599
|
+
* Supported terminals respond with CSI ? flags u.
|
|
1600
|
+
* Unsupported terminals either ignore the query or echo it.
|
|
1601
|
+
*
|
|
1602
|
+
* @param write Function to write to stdout
|
|
1603
|
+
* @param read Function to read a chunk from stdin (should resolve with data or null on timeout)
|
|
1604
|
+
* @param timeoutMs How long to wait for response (default: 200ms)
|
|
1605
|
+
*/
|
|
1606
|
+
async function detectKittySupport(write, read, timeoutMs = 200) {
|
|
1607
|
+
write(queryKittyKeyboard());
|
|
1608
|
+
const data = await read(timeoutMs);
|
|
1609
|
+
if (data == null) return {
|
|
1610
|
+
supported: false,
|
|
1611
|
+
flags: 0
|
|
1612
|
+
};
|
|
1613
|
+
const parsed = parseKittyResponse(data);
|
|
1614
|
+
if (!parsed) return {
|
|
1615
|
+
supported: false,
|
|
1616
|
+
flags: 0,
|
|
1617
|
+
buffered: data
|
|
1618
|
+
};
|
|
1619
|
+
const buffered = data.slice(0, parsed.start) + data.slice(parsed.end);
|
|
1620
|
+
return {
|
|
1621
|
+
...parsed.result,
|
|
1622
|
+
buffered: buffered || void 0
|
|
1623
|
+
};
|
|
1624
|
+
}
|
|
1625
|
+
/** Query Kitty support through the session's canonical stdin owner. */
|
|
1626
|
+
async function detectKittyWithProbe(input, timeoutMs = 200) {
|
|
1627
|
+
return await input.probe({
|
|
1628
|
+
query: queryKittyKeyboard(),
|
|
1629
|
+
timeoutMs,
|
|
1630
|
+
parse: (acc) => {
|
|
1631
|
+
const parsed = parseKittyResponse(acc);
|
|
1632
|
+
return parsed ? {
|
|
1633
|
+
result: parsed.result,
|
|
1634
|
+
consumed: parsed.end
|
|
1635
|
+
} : null;
|
|
1636
|
+
}
|
|
1637
|
+
}) ?? {
|
|
1638
|
+
supported: false,
|
|
1639
|
+
flags: 0
|
|
1640
|
+
};
|
|
1641
|
+
}
|
|
1642
|
+
function parseKittyResponse(data) {
|
|
1643
|
+
const match = KITTY_RESPONSE_RE.exec(data);
|
|
1644
|
+
if (!match) return null;
|
|
1645
|
+
return {
|
|
1646
|
+
result: {
|
|
1647
|
+
supported: true,
|
|
1648
|
+
flags: parseInt(match[1], 10)
|
|
1649
|
+
},
|
|
1650
|
+
start: match.index,
|
|
1651
|
+
end: match.index + match[0].length
|
|
1652
|
+
};
|
|
1653
|
+
}
|
|
1654
|
+
/**
|
|
1655
|
+
* Detect Kitty support using real stdin/stdout.
|
|
1656
|
+
* Convenience wrapper around detectKittySupport.
|
|
1657
|
+
*/
|
|
1658
|
+
async function detectKittyFromStdio(stdout, stdin, timeoutMs = 200) {
|
|
1659
|
+
const otherListeners = stdin.listenerCount("data") > 0;
|
|
1660
|
+
const wasRaw = stdin.isRaw;
|
|
1661
|
+
let didSetRaw = false;
|
|
1662
|
+
if (!wasRaw && !otherListeners) {
|
|
1663
|
+
stdin.setRawMode(true);
|
|
1664
|
+
didSetRaw = true;
|
|
1665
|
+
}
|
|
1666
|
+
try {
|
|
1667
|
+
const write = (s) => {
|
|
1668
|
+
stdout.write(s);
|
|
1669
|
+
};
|
|
1670
|
+
const read = (ms) => new Promise((resolve) => {
|
|
1671
|
+
const timer = setTimeout(() => {
|
|
1672
|
+
stdin.removeListener("data", onData);
|
|
1673
|
+
resolve(null);
|
|
1674
|
+
}, ms);
|
|
1675
|
+
function onData(chunk) {
|
|
1676
|
+
clearTimeout(timer);
|
|
1677
|
+
stdin.removeListener("data", onData);
|
|
1678
|
+
resolve(chunk.toString());
|
|
1679
|
+
}
|
|
1680
|
+
stdin.on("data", onData);
|
|
1681
|
+
});
|
|
1682
|
+
return await detectKittySupport(write, read, timeoutMs);
|
|
1683
|
+
} finally {
|
|
1684
|
+
if (didSetRaw) stdin.setRawMode(false);
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
//#endregion
|
|
1688
|
+
//#region packages/ag-term/src/termtest.ts
|
|
1689
|
+
/**
|
|
1690
|
+
* Terminal Capability Test
|
|
1691
|
+
*
|
|
1692
|
+
* Renders labeled test patterns for each terminal feature.
|
|
1693
|
+
* Run in any terminal to visually verify what it supports.
|
|
1694
|
+
*
|
|
1695
|
+
* Usage:
|
|
1696
|
+
* import { runTermtest } from "@silvery/ag-react"
|
|
1697
|
+
* runTermtest() // all sections
|
|
1698
|
+
* runTermtest({ sections: ["emoji", "colors"] }) // specific sections
|
|
1699
|
+
*
|
|
1700
|
+
* Compare output across terminals to build/verify profiles.
|
|
1701
|
+
*/
|
|
1702
|
+
const CSI = `[`;
|
|
1703
|
+
const RESET = `${CSI}0m`;
|
|
1704
|
+
function sgr(...codes) {
|
|
1705
|
+
return `${CSI}${codes.join(";")}m`;
|
|
1706
|
+
}
|
|
1707
|
+
function sectionHeader(title) {
|
|
1708
|
+
return `\n${sgr(1)}═══ ${title} ═══${RESET}\n`;
|
|
1709
|
+
}
|
|
1710
|
+
function row(label, content) {
|
|
1711
|
+
return ` ${label.padEnd(24)} ${content}${RESET}`;
|
|
1712
|
+
}
|
|
1713
|
+
/** Available test sections */
|
|
1714
|
+
const TERMTEST_SECTIONS = [
|
|
1715
|
+
"sgr",
|
|
1716
|
+
"underline",
|
|
1717
|
+
"colors",
|
|
1718
|
+
"256",
|
|
1719
|
+
"truecolor",
|
|
1720
|
+
"unicode",
|
|
1721
|
+
"emoji",
|
|
1722
|
+
"borders",
|
|
1723
|
+
"inverse",
|
|
1724
|
+
"profile"
|
|
1725
|
+
];
|
|
1726
|
+
/**
|
|
1727
|
+
* Run the terminal capability test.
|
|
1728
|
+
* Pass section names to filter: `runTermtest({ sections: ["emoji"] })`
|
|
1729
|
+
*/
|
|
1730
|
+
function runTermtest(options) {
|
|
1731
|
+
const w = options?.output ?? process.stdout;
|
|
1732
|
+
const filter = options?.sections;
|
|
1733
|
+
const show = (s) => !filter || filter.length === 0 || filter.includes(s);
|
|
1734
|
+
const { caps, emulator } = createTerminalProfile();
|
|
1735
|
+
w.write(`\n${sgr(1)}Terminal Capability Test${RESET}\n`);
|
|
1736
|
+
w.write(` Program: ${emulator.program || "(unknown)"}\n`);
|
|
1737
|
+
w.write(` TERM: ${emulator.TERM || "(unknown)"}\n`);
|
|
1738
|
+
w.write(` COLORTERM: ${process.env.COLORTERM || "(unset)"}\n`);
|
|
1739
|
+
w.write(` Detected: color=${caps.colorLevel} maybe-dark=${caps.maybeDarkBackground} maybe-nerdfont=${caps.maybeNerdFont}\n`);
|
|
1740
|
+
w.write(` Underline: styles=[${caps.underlineStyles.join(", ")}] color=${caps.underlineColor}\n`);
|
|
1741
|
+
w.write(` Maybe wide emojis: ${caps.maybeWideEmojis}\n`);
|
|
1742
|
+
if (show("sgr")) {
|
|
1743
|
+
w.write(sectionHeader("SGR Text Attributes"));
|
|
1744
|
+
w.write(row("Bold", `${sgr(1)}The quick brown fox${RESET}`) + "\n");
|
|
1745
|
+
w.write(row("Dim", `${sgr(2)}The quick brown fox${RESET}`) + "\n");
|
|
1746
|
+
w.write(row("Italic", `${sgr(3)}The quick brown fox${RESET}`) + "\n");
|
|
1747
|
+
w.write(row("Underline", `${sgr(4)}The quick brown fox${RESET}`) + "\n");
|
|
1748
|
+
w.write(row("Strikethrough", `${sgr(9)}The quick brown fox${RESET}`) + "\n");
|
|
1749
|
+
w.write(row("Inverse", `${sgr(7)}The quick brown fox${RESET}`) + "\n");
|
|
1750
|
+
w.write(row("Blink", `${sgr(5)}The quick brown fox${RESET}`) + "\n");
|
|
1751
|
+
w.write(row("Bold+Italic", `${sgr(1, 3)}The quick brown fox${RESET}`) + "\n");
|
|
1752
|
+
}
|
|
1753
|
+
if (show("underline")) {
|
|
1754
|
+
w.write(sectionHeader("SGR 4:x Underline Styles (Terminal.app BREAKS here)"));
|
|
1755
|
+
w.write(row("4:1 Single", `${CSI}4:1mThe quick brown fox${RESET}`) + "\n");
|
|
1756
|
+
w.write(row("4:2 Double", `${CSI}4:2mThe quick brown fox${RESET}`) + "\n");
|
|
1757
|
+
w.write(row("4:3 Curly", `${CSI}4:3mThe quick brown fox${RESET}`) + "\n");
|
|
1758
|
+
w.write(row("4:4 Dotted", `${CSI}4:4mThe quick brown fox${RESET}`) + "\n");
|
|
1759
|
+
w.write(row("4:5 Dashed", `${CSI}4:5mThe quick brown fox${RESET}`) + "\n");
|
|
1760
|
+
w.write(row("After 4:x (clean?)", `${CSI}4:3m${RESET}This text should be normal — if garbled, 4:x corrupted SGR state`) + "\n");
|
|
1761
|
+
w.write(sectionHeader("SGR 58 Underline Color (Terminal.app BREAKS here)"));
|
|
1762
|
+
w.write(row("58;5;1 Red UL", `${sgr(4)}${CSI}58;5;1mThe quick brown fox${RESET}`) + "\n");
|
|
1763
|
+
w.write(row("58;5;2 Green UL", `${sgr(4)}${CSI}58;5;2mThe quick brown fox${RESET}`) + "\n");
|
|
1764
|
+
w.write(row("58;5;4 Blue UL", `${sgr(4)}${CSI}58;5;4mThe quick brown fox${RESET}`) + "\n");
|
|
1765
|
+
w.write(row("58;2;R;G;B TC UL", `${sgr(4)}${CSI}58;2;255;128;0mThe quick brown fox${RESET}`) + "\n");
|
|
1766
|
+
w.write(row("After SGR 58 (clean?)", `${sgr(4)}${CSI}58;5;1m${RESET}This text should be normal — if garbled, 58 corrupted SGR state`) + "\n");
|
|
1767
|
+
}
|
|
1768
|
+
if (show("colors")) {
|
|
1769
|
+
w.write(sectionHeader("ANSI 16 Colors"));
|
|
1770
|
+
const colorNames = [
|
|
1771
|
+
"Black",
|
|
1772
|
+
"Red",
|
|
1773
|
+
"Green",
|
|
1774
|
+
"Yellow",
|
|
1775
|
+
"Blue",
|
|
1776
|
+
"Magenta",
|
|
1777
|
+
"Cyan",
|
|
1778
|
+
"White"
|
|
1779
|
+
];
|
|
1780
|
+
let fgLine = " FG: ";
|
|
1781
|
+
for (let i = 0; i < 8; i++) fgLine += `${sgr(30 + i)} ${colorNames[i]}${RESET}`;
|
|
1782
|
+
w.write(fgLine + "\n");
|
|
1783
|
+
let fgBrLine = " Br: ";
|
|
1784
|
+
for (let i = 0; i < 8; i++) fgBrLine += `${sgr(90 + i)} ${colorNames[i]}${RESET}`;
|
|
1785
|
+
w.write(fgBrLine + "\n");
|
|
1786
|
+
let bgLine = " BG: ";
|
|
1787
|
+
for (let i = 0; i < 8; i++) bgLine += `${sgr(40 + i)} ${colorNames[i]} ${RESET}`;
|
|
1788
|
+
w.write(bgLine + "\n");
|
|
1789
|
+
let bgBrLine = " BrBG:";
|
|
1790
|
+
for (let i = 0; i < 8; i++) bgBrLine += `${sgr(100 + i)} ${colorNames[i]} ${RESET}`;
|
|
1791
|
+
w.write(bgBrLine + "\n");
|
|
1792
|
+
}
|
|
1793
|
+
if (show("256")) {
|
|
1794
|
+
w.write(sectionHeader("256 Colors (indices 0-15, 16-231, 232-255)"));
|
|
1795
|
+
let stdLine = " 0-15: ";
|
|
1796
|
+
for (let i = 0; i < 16; i++) stdLine += `${CSI}48;5;${i}m ${RESET}`;
|
|
1797
|
+
w.write(stdLine + "\n");
|
|
1798
|
+
let cubeLine = " Cube: ";
|
|
1799
|
+
for (let i = 16; i < 52; i++) cubeLine += `${CSI}48;5;${i}m ${RESET}`;
|
|
1800
|
+
w.write(cubeLine + "\n");
|
|
1801
|
+
let grayLine = " Gray: ";
|
|
1802
|
+
for (let i = 232; i < 256; i++) grayLine += `${CSI}48;5;${i}m ${RESET}`;
|
|
1803
|
+
w.write(grayLine + "\n");
|
|
1804
|
+
}
|
|
1805
|
+
if (show("truecolor")) {
|
|
1806
|
+
w.write(sectionHeader("Truecolor (38;2;R;G;B / 48;2;R;G;B)"));
|
|
1807
|
+
let tcLine = " Gradient: ";
|
|
1808
|
+
for (let i = 0; i < 40; i++) {
|
|
1809
|
+
const r = Math.round(i / 39 * 255);
|
|
1810
|
+
const g = Math.round((39 - i) / 39 * 128);
|
|
1811
|
+
tcLine += `${CSI}48;2;${r};${g};80m ${RESET}`;
|
|
1812
|
+
}
|
|
1813
|
+
w.write(tcLine + "\n");
|
|
1814
|
+
w.write(row("If solid blocks →", "Truecolor NOT supported (256-color fallback)") + "\n");
|
|
1815
|
+
}
|
|
1816
|
+
if (show("unicode")) {
|
|
1817
|
+
w.write(sectionHeader("Unicode, Emoji, PUA (Nerd Fonts)"));
|
|
1818
|
+
w.write(row("ASCII", "Hello World! 0123456789") + "\n");
|
|
1819
|
+
w.write(row("Latin Extended", "àéîõü ñ ß ø å") + "\n");
|
|
1820
|
+
w.write(row("CJK", "你好世界 日本語 한국어") + "\n");
|
|
1821
|
+
w.write(row("Box Drawing", "┌─┬─┐ ╔═╦═╗ ╭─╮") + "\n");
|
|
1822
|
+
w.write(row("Block Elements", "▀▄█▌▐░▒▓") + "\n");
|
|
1823
|
+
w.write(row("Symbols", "● ○ ◉ ▶ ◀ ⚠ ✓ ✗ ⋮ §") + "\n");
|
|
1824
|
+
w.write(row("Emoji", "🎉 🚀 📁 📄 ⭐ 🔥 👍") + "\n");
|
|
1825
|
+
w.write(row("Nerd Font PUA", " folder file arrow gear") + "\n");
|
|
1826
|
+
w.write(row("If PUA = boxes →", "Nerd Fonts not installed") + "\n");
|
|
1827
|
+
}
|
|
1828
|
+
if (show("emoji")) {
|
|
1829
|
+
w.write(sectionHeader("Emoji Width Alignment (_'s should align with ruler)"));
|
|
1830
|
+
w.write(" Ruler: |1234567890|\n");
|
|
1831
|
+
w.write(" ASCII 'A': |A_________| (w=1)\n");
|
|
1832
|
+
w.write(" CJK '你': |你________| (w=2)\n");
|
|
1833
|
+
w.write(" Flag 🇨🇦: |🇨🇦________| (w=2)\n");
|
|
1834
|
+
w.write(" Flag 🇺🇸: |🇺🇸________| (w=2)\n");
|
|
1835
|
+
w.write(" Emoji 📁: |📁________| (w=2)\n");
|
|
1836
|
+
w.write(" Emoji 📄: |📄________| (w=2)\n");
|
|
1837
|
+
w.write(" Emoji 📋: |📋________| (w=2)\n");
|
|
1838
|
+
w.write(" Emoji ⚠️: |⚠️________| (w=2)\n");
|
|
1839
|
+
w.write(" Text ⚠: |⚠_________| (w=1 text, 2 emoji)\n");
|
|
1840
|
+
w.write(" Text ⭐: |⭐________| (w=1 text, 2 emoji)\n");
|
|
1841
|
+
w.write(" Text ☑: |☑_________| (w=1 text, 2 emoji)\n");
|
|
1842
|
+
w.write(" Emoji 🏠: |🏠________| (w=2)\n");
|
|
1843
|
+
w.write(" Emoji 👓: |👓________| (w=2)\n");
|
|
1844
|
+
w.write(" ZWJ 👨🏻💻: |👨🏻💻________| (w=2)\n");
|
|
1845
|
+
w.write(" Arrow →: |→_________| (w=1)\n");
|
|
1846
|
+
w.write(" Arrow ▸: |▸_________| (w=1)\n");
|
|
1847
|
+
w.write(" Circle ○: |○_________| (w=1)\n");
|
|
1848
|
+
w.write(" Square □: |□_________| (w=1)\n");
|
|
1849
|
+
w.write(" Check ✓: |✓_________| (w=1)\n");
|
|
1850
|
+
}
|
|
1851
|
+
if (show("borders")) {
|
|
1852
|
+
w.write(sectionHeader("Box Drawing Borders"));
|
|
1853
|
+
w.write(" ┌──────────┐ ╔══════════╗ ╭──────────╮\n");
|
|
1854
|
+
w.write(" │ single │ ║ double ║ │ round │\n");
|
|
1855
|
+
w.write(" └──────────┘ ╚══════════╝ ╰──────────╯\n");
|
|
1856
|
+
}
|
|
1857
|
+
if (show("inverse")) {
|
|
1858
|
+
w.write(sectionHeader("Inverse + Background (potential artifact source)"));
|
|
1859
|
+
w.write(row("Red FG + Inverse", `${sgr(31, 7)}This should have red background${RESET}`) + "\n");
|
|
1860
|
+
w.write(row("Cyan BG + White FG", `${sgr(46, 37)}Cyan background, white text${RESET}`) + "\n");
|
|
1861
|
+
w.write(row("Black BG + White FG", `${sgr(40, 97)}Black bg, bright white text${RESET}`) + "\n");
|
|
1862
|
+
w.write(row("White BG + Black FG", `${sgr(107, 30)}White bg, black text${RESET}`) + "\n");
|
|
1863
|
+
w.write(sectionHeader("Reset Sanity Check"));
|
|
1864
|
+
w.write(" This line should be completely normal with no formatting artifacts.\n");
|
|
1865
|
+
w.write(" If you see colors, underlines, or other styling above, the terminal\n");
|
|
1866
|
+
w.write(" failed to process an SGR reset (\\x1b[0m) correctly.\n");
|
|
1867
|
+
}
|
|
1868
|
+
if (show("profile")) {
|
|
1869
|
+
w.write(sectionHeader("Detected Terminal Profile"));
|
|
1870
|
+
const entries = Object.entries(caps);
|
|
1871
|
+
for (const [key, value] of entries) {
|
|
1872
|
+
const indicator = value === true ? "✓" : value === false ? "✗" : String(value);
|
|
1873
|
+
w.write(` ${key.padEnd(20)} ${indicator}\n`);
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
w.write("\n");
|
|
1877
|
+
}
|
|
1878
|
+
//#endregion
|
|
1879
|
+
//#region packages/ag-term/src/cursor-query.ts
|
|
1880
|
+
/**
|
|
1881
|
+
* CSI 6n Cursor Position Query
|
|
1882
|
+
*
|
|
1883
|
+
* Queries the terminal for the current cursor position using the standard
|
|
1884
|
+
* Device Status Report (DSR) mechanism.
|
|
1885
|
+
*
|
|
1886
|
+
* Protocol:
|
|
1887
|
+
* - Query: CSI 6 n (\x1b[6n)
|
|
1888
|
+
* - Response: CSI {row} ; {col} R (\x1b[{row};{col}R)
|
|
1889
|
+
*
|
|
1890
|
+
* Row and column are 1-indexed in the protocol response.
|
|
1891
|
+
*
|
|
1892
|
+
* Supported by: virtually all terminals (VT100+)
|
|
1893
|
+
*/
|
|
1894
|
+
/** Regex to match a CPR response: CSI row ; col R */
|
|
1895
|
+
const CPR_RESPONSE_RE = /\x1b\[(\d+);(\d+)R/;
|
|
1896
|
+
/**
|
|
1897
|
+
* Query the terminal cursor position.
|
|
1898
|
+
*
|
|
1899
|
+
* Sends CSI 6n and parses the CPR response.
|
|
1900
|
+
* Returns 1-indexed row and column.
|
|
1901
|
+
*
|
|
1902
|
+
* @param write Function to write to stdout
|
|
1903
|
+
* @param read Function to read a chunk from stdin (resolves with data or null on timeout)
|
|
1904
|
+
* @param timeoutMs How long to wait for response (default: 200ms)
|
|
1905
|
+
*/
|
|
1906
|
+
async function queryCursorPosition(write, read, timeoutMs = 200) {
|
|
1907
|
+
write("\x1B[6n");
|
|
1908
|
+
const data = await read(timeoutMs);
|
|
1909
|
+
if (data == null) return null;
|
|
1910
|
+
const match = CPR_RESPONSE_RE.exec(data);
|
|
1911
|
+
if (!match) return null;
|
|
1912
|
+
return {
|
|
1913
|
+
row: parseInt(match[1], 10),
|
|
1914
|
+
col: parseInt(match[2], 10)
|
|
1915
|
+
};
|
|
1916
|
+
}
|
|
1917
|
+
/**
|
|
1918
|
+
* Query cursor position using real stdin/stdout.
|
|
1919
|
+
* Convenience wrapper around queryCursorPosition.
|
|
1920
|
+
*/
|
|
1921
|
+
async function queryCursorFromStdio(stdout, stdin, timeoutMs = 200) {
|
|
1922
|
+
const otherListeners = stdin.listenerCount("data") > 0;
|
|
1923
|
+
const wasRaw = stdin.isRaw;
|
|
1924
|
+
let didSetRaw = false;
|
|
1925
|
+
if (!wasRaw && !otherListeners) {
|
|
1926
|
+
stdin.setRawMode(true);
|
|
1927
|
+
didSetRaw = true;
|
|
1928
|
+
}
|
|
1929
|
+
try {
|
|
1930
|
+
const write = (s) => {
|
|
1931
|
+
stdout.write(s);
|
|
1932
|
+
};
|
|
1933
|
+
const read = (ms) => new Promise((resolve) => {
|
|
1934
|
+
const timer = setTimeout(() => {
|
|
1935
|
+
stdin.removeListener("data", onData);
|
|
1936
|
+
resolve(null);
|
|
1937
|
+
}, ms);
|
|
1938
|
+
function onData(chunk) {
|
|
1939
|
+
clearTimeout(timer);
|
|
1940
|
+
stdin.removeListener("data", onData);
|
|
1941
|
+
resolve(chunk.toString());
|
|
1942
|
+
}
|
|
1943
|
+
stdin.on("data", onData);
|
|
1944
|
+
});
|
|
1945
|
+
return await queryCursorPosition(write, read, timeoutMs);
|
|
1946
|
+
} finally {
|
|
1947
|
+
if (didSetRaw) stdin.setRawMode(false);
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
//#endregion
|
|
1951
|
+
//#region packages/ag-term/src/ansi/width-detection.ts
|
|
1952
|
+
/** Well-known xterm width DEC mode numbers. */
|
|
1953
|
+
const WidthMode = {
|
|
1954
|
+
UTF8: 1020,
|
|
1955
|
+
CJK_WIDTH: 1021,
|
|
1956
|
+
EMOJI_WIDTH: 1022,
|
|
1957
|
+
PRIVATE_USE_WIDTH: 1023
|
|
1958
|
+
};
|
|
1959
|
+
/** Default configuration when detection fails or times out. */
|
|
1960
|
+
const DEFAULT_WIDTH_CONFIG = {
|
|
1961
|
+
utf8: true,
|
|
1962
|
+
cjkWidth: 1,
|
|
1963
|
+
emojiWidth: 2,
|
|
1964
|
+
privateUseWidth: 1
|
|
1965
|
+
};
|
|
1966
|
+
/** Regex for DECRPM response: CSI ? mode ; Ps $ y */
|
|
1967
|
+
const DECRPM_RE = /\x1b\[\?(\d+);(\d+)\$y/g;
|
|
1968
|
+
/**
|
|
1969
|
+
* Parse a DECRPM response value into a boolean (set/reset).
|
|
1970
|
+
* 1 and 3 (permanently set) = true, everything else = false.
|
|
1971
|
+
*/
|
|
1972
|
+
function isSet(ps) {
|
|
1973
|
+
return ps === 1 || ps === 3;
|
|
1974
|
+
}
|
|
1975
|
+
/**
|
|
1976
|
+
* Query width modes through the session's canonical stdin owner.
|
|
1977
|
+
*
|
|
1978
|
+
* Unlike `createWidthDetector`'s standalone transport adapter, this path
|
|
1979
|
+
* never attaches another `data` listener: responses are consumed by the
|
|
1980
|
+
* owner's probe lane before normal key/mouse parsing.
|
|
1981
|
+
*/
|
|
1982
|
+
function detectWidthConfigWithProbe(input, timeoutMs = 200) {
|
|
1983
|
+
return detectWidthConfig((mode) => input.probe({
|
|
1984
|
+
query: `\x1b[?${mode}$p`,
|
|
1985
|
+
timeoutMs,
|
|
1986
|
+
parse: (acc) => parseWidthModeResponse(acc, mode)
|
|
1987
|
+
}));
|
|
1988
|
+
}
|
|
1989
|
+
function parseWidthModeResponse(acc, expectedMode) {
|
|
1990
|
+
DECRPM_RE.lastIndex = 0;
|
|
1991
|
+
let match;
|
|
1992
|
+
while ((match = DECRPM_RE.exec(acc)) !== null) {
|
|
1993
|
+
if (parseInt(match[1], 10) !== expectedMode) continue;
|
|
1994
|
+
return {
|
|
1995
|
+
result: parseInt(match[2], 10),
|
|
1996
|
+
consumed: match.index + match[0].length
|
|
1997
|
+
};
|
|
1998
|
+
}
|
|
1999
|
+
return null;
|
|
2000
|
+
}
|
|
2001
|
+
async function detectWidthConfig(query) {
|
|
2002
|
+
const utf8Ps = await query(WidthMode.UTF8);
|
|
2003
|
+
const cjkPs = await query(WidthMode.CJK_WIDTH);
|
|
2004
|
+
const emojiPs = await query(WidthMode.EMOJI_WIDTH);
|
|
2005
|
+
const puaPs = await query(WidthMode.PRIVATE_USE_WIDTH);
|
|
2006
|
+
return {
|
|
2007
|
+
utf8: utf8Ps !== null ? isSet(utf8Ps) : DEFAULT_WIDTH_CONFIG.utf8,
|
|
2008
|
+
cjkWidth: cjkPs !== null ? isSet(cjkPs) ? 2 : 1 : DEFAULT_WIDTH_CONFIG.cjkWidth,
|
|
2009
|
+
emojiWidth: emojiPs !== null ? isSet(emojiPs) ? 2 : 1 : DEFAULT_WIDTH_CONFIG.emojiWidth,
|
|
2010
|
+
privateUseWidth: puaPs !== null ? isSet(puaPs) ? 2 : 1 : DEFAULT_WIDTH_CONFIG.privateUseWidth
|
|
2011
|
+
};
|
|
2012
|
+
}
|
|
2013
|
+
/**
|
|
2014
|
+
* Apply detected width configuration to terminal capabilities.
|
|
2015
|
+
*
|
|
2016
|
+
* Post km-silvery.plateau-naming-polish (2026-04-23): `maybeWideEmojis` and
|
|
2017
|
+
* `textSizing` both live on caps, so the overlay returns a single updated
|
|
2018
|
+
* caps object. The mapping is:
|
|
2019
|
+
* - emojiWidth=2 → caps.maybeWideEmojis=true
|
|
2020
|
+
* - privateUseWidth=2 → caps.textSizing=true (PUA treated as 2-wide)
|
|
2021
|
+
*
|
|
2022
|
+
* CJK width and UTF-8 mode are informational — they don't yet map to caps
|
|
2023
|
+
* fields but are available in TerminalWidthConfig for consumers.
|
|
2024
|
+
*/
|
|
2025
|
+
function applyWidthConfig(caps, config) {
|
|
2026
|
+
return {
|
|
2027
|
+
...caps,
|
|
2028
|
+
textSizing: config.privateUseWidth === 2,
|
|
2029
|
+
maybeWideEmojis: config.emojiWidth === 2
|
|
2030
|
+
};
|
|
2031
|
+
}
|
|
2032
|
+
//#endregion
|
|
2033
|
+
//#region packages/ag-term/src/device-attrs.ts
|
|
2034
|
+
/**
|
|
2035
|
+
* Device Attributes (DA1/DA2/DA3) + XTVERSION Queries
|
|
2036
|
+
*
|
|
2037
|
+
* Provides functions to query terminal identity and capabilities using
|
|
2038
|
+
* the standard VT device attribute escape sequences.
|
|
2039
|
+
*
|
|
2040
|
+
* Protocols:
|
|
2041
|
+
*
|
|
2042
|
+
* DA1 (Primary Device Attributes):
|
|
2043
|
+
* Query: CSI c (\x1b[c)
|
|
2044
|
+
* Response: CSI ? Ps ; Ps ; ... c
|
|
2045
|
+
*
|
|
2046
|
+
* DA2 (Secondary Device Attributes):
|
|
2047
|
+
* Query: CSI > c (\x1b[>c)
|
|
2048
|
+
* Response: CSI > Pt ; Pv ; Pc c
|
|
2049
|
+
* Where Pt=terminal type, Pv=firmware version, Pc=ROM cartridge id
|
|
2050
|
+
*
|
|
2051
|
+
* DA3 (Tertiary Device Attributes):
|
|
2052
|
+
* Query: CSI = c (\x1b[=c)
|
|
2053
|
+
* Response: DCS ! | hex-encoded-id ST (\x1bP!|{hex}\x1b\\)
|
|
2054
|
+
*
|
|
2055
|
+
* XTVERSION (Terminal Name + Version):
|
|
2056
|
+
* Query: CSI > 0 q (\x1b[>0q)
|
|
2057
|
+
* Response: DCS > | name(version) ST (\x1bP>|{text}\x1b\\)
|
|
2058
|
+
*
|
|
2059
|
+
* Supported by: xterm, Ghostty, Kitty, WezTerm, foot, VTE-based terminals
|
|
2060
|
+
*/
|
|
2061
|
+
/** Regex for DA1 response: CSI ? params c */
|
|
2062
|
+
const DA1_RESPONSE_RE = /\x1b\[\?([\d;]+)c/;
|
|
2063
|
+
/** Regex for DA2 response: CSI > params c */
|
|
2064
|
+
const DA2_RESPONSE_RE = /\x1b\[>([\d;]+)c/;
|
|
2065
|
+
/** Regex for DA3 response: DCS ! | hex ST */
|
|
2066
|
+
const DA3_RESPONSE_RE = /\x1bP!\|([0-9a-fA-F]*)\x1b\\/;
|
|
2067
|
+
/** Regex for XTVERSION response: DCS > | text ST */
|
|
2068
|
+
const XTVERSION_RESPONSE_RE = /\x1bP>\|([^\x1b]*)\x1b\\/;
|
|
2069
|
+
/**
|
|
2070
|
+
* Query primary device attributes (DA1).
|
|
2071
|
+
*
|
|
2072
|
+
* Returns the list of attribute parameters the terminal reports.
|
|
2073
|
+
* Common params: 1=132-cols, 4=sixel, 6=selective-erase, 22=ANSI-color
|
|
2074
|
+
*
|
|
2075
|
+
* @param write Function to write to stdout
|
|
2076
|
+
* @param read Function to read a chunk from stdin
|
|
2077
|
+
* @param timeoutMs How long to wait for response (default: 200ms)
|
|
2078
|
+
*/
|
|
2079
|
+
async function queryPrimaryDA(write, read, timeoutMs = 200) {
|
|
2080
|
+
write("\x1B[c");
|
|
2081
|
+
const data = await read(timeoutMs);
|
|
2082
|
+
if (data == null) return null;
|
|
2083
|
+
const match = DA1_RESPONSE_RE.exec(data);
|
|
2084
|
+
if (!match) return null;
|
|
2085
|
+
return { params: match[1].split(";").map((s) => parseInt(s, 10)) };
|
|
2086
|
+
}
|
|
2087
|
+
/**
|
|
2088
|
+
* Query secondary device attributes (DA2).
|
|
2089
|
+
*
|
|
2090
|
+
* Returns terminal type, firmware version, and ROM cartridge id.
|
|
2091
|
+
* Common type values: 0=VT100, 1=VT220, 41=xterm, 65=VT500
|
|
2092
|
+
*
|
|
2093
|
+
* @param write Function to write to stdout
|
|
2094
|
+
* @param read Function to read a chunk from stdin
|
|
2095
|
+
* @param timeoutMs How long to wait for response (default: 200ms)
|
|
2096
|
+
*/
|
|
2097
|
+
async function querySecondaryDA(write, read, timeoutMs = 200) {
|
|
2098
|
+
write("\x1B[>c");
|
|
2099
|
+
const data = await read(timeoutMs);
|
|
2100
|
+
if (data == null) return null;
|
|
2101
|
+
const match = DA2_RESPONSE_RE.exec(data);
|
|
2102
|
+
if (!match) return null;
|
|
2103
|
+
const parts = match[1].split(";");
|
|
2104
|
+
if (parts.length < 3) return null;
|
|
2105
|
+
return {
|
|
2106
|
+
type: parseInt(parts[0], 10),
|
|
2107
|
+
version: parseInt(parts[1], 10),
|
|
2108
|
+
id: parseInt(parts[2], 10)
|
|
2109
|
+
};
|
|
2110
|
+
}
|
|
2111
|
+
/**
|
|
2112
|
+
* Query tertiary device attributes (DA3).
|
|
2113
|
+
*
|
|
2114
|
+
* Returns a hex-encoded unit ID string. Decode with Buffer.from(hex, 'hex').
|
|
2115
|
+
*
|
|
2116
|
+
* @param write Function to write to stdout
|
|
2117
|
+
* @param read Function to read a chunk from stdin
|
|
2118
|
+
* @param timeoutMs How long to wait for response (default: 200ms)
|
|
2119
|
+
*/
|
|
2120
|
+
async function queryTertiaryDA(write, read, timeoutMs = 200) {
|
|
2121
|
+
write("\x1B[=c");
|
|
2122
|
+
const data = await read(timeoutMs);
|
|
2123
|
+
if (data == null) return null;
|
|
2124
|
+
const match = DA3_RESPONSE_RE.exec(data);
|
|
2125
|
+
if (!match) return null;
|
|
2126
|
+
return match[1];
|
|
2127
|
+
}
|
|
2128
|
+
/**
|
|
2129
|
+
* Query the terminal name and version via XTVERSION.
|
|
2130
|
+
*
|
|
2131
|
+
* Returns the version string as reported by the terminal, e.g.:
|
|
2132
|
+
* - "xterm(388)"
|
|
2133
|
+
* - "tmux 3.4"
|
|
2134
|
+
* - "WezTerm 20230712-072601-f4abf8fd"
|
|
2135
|
+
*
|
|
2136
|
+
* @param write Function to write to stdout
|
|
2137
|
+
* @param read Function to read a chunk from stdin
|
|
2138
|
+
* @param timeoutMs How long to wait for response (default: 200ms)
|
|
2139
|
+
*/
|
|
2140
|
+
async function queryTerminalVersion(write, read, timeoutMs = 200) {
|
|
2141
|
+
write("\x1B[>0q");
|
|
2142
|
+
const data = await read(timeoutMs);
|
|
2143
|
+
if (data == null) return null;
|
|
2144
|
+
const match = XTVERSION_RESPONSE_RE.exec(data);
|
|
2145
|
+
if (!match) return null;
|
|
2146
|
+
return match[1];
|
|
2147
|
+
}
|
|
2148
|
+
/**
|
|
2149
|
+
* Query all device attributes: DA1, DA2, and XTVERSION.
|
|
2150
|
+
*
|
|
2151
|
+
* Convenience wrapper that queries all three sequentially.
|
|
2152
|
+
* DA3 is omitted from the combined query as it's rarely needed.
|
|
2153
|
+
*
|
|
2154
|
+
* @param stdout Writable stream (e.g., process.stdout)
|
|
2155
|
+
* @param stdin Readable stream (e.g., process.stdin)
|
|
2156
|
+
* @param timeoutMs Per-query timeout (default: 200ms)
|
|
2157
|
+
*/
|
|
2158
|
+
async function queryDeviceAttributes(stdout, stdin, timeoutMs = 200) {
|
|
2159
|
+
const otherListeners = stdin.listenerCount("data") > 0;
|
|
2160
|
+
const wasRaw = stdin.isRaw;
|
|
2161
|
+
let didSetRaw = false;
|
|
2162
|
+
if (!wasRaw && !otherListeners) {
|
|
2163
|
+
stdin.setRawMode(true);
|
|
2164
|
+
didSetRaw = true;
|
|
2165
|
+
}
|
|
2166
|
+
try {
|
|
2167
|
+
const write = (s) => {
|
|
2168
|
+
stdout.write(s);
|
|
2169
|
+
};
|
|
2170
|
+
const read = (ms) => new Promise((resolve) => {
|
|
2171
|
+
const timer = setTimeout(() => {
|
|
2172
|
+
stdin.removeListener("data", onData);
|
|
2173
|
+
resolve(null);
|
|
2174
|
+
}, ms);
|
|
2175
|
+
function onData(chunk) {
|
|
2176
|
+
clearTimeout(timer);
|
|
2177
|
+
stdin.removeListener("data", onData);
|
|
2178
|
+
resolve(chunk.toString());
|
|
2179
|
+
}
|
|
2180
|
+
stdin.on("data", onData);
|
|
2181
|
+
});
|
|
2182
|
+
return {
|
|
2183
|
+
da1: await queryPrimaryDA(write, read, timeoutMs),
|
|
2184
|
+
da2: await querySecondaryDA(write, read, timeoutMs),
|
|
2185
|
+
version: await queryTerminalVersion(write, read, timeoutMs)
|
|
2186
|
+
};
|
|
2187
|
+
} finally {
|
|
2188
|
+
if (didSetRaw) stdin.setRawMode(false);
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
//#endregion
|
|
2192
|
+
//#region packages/ag-term/src/mode-query.ts
|
|
2193
|
+
/**
|
|
2194
|
+
* DECRQM — DEC Private Mode Query
|
|
2195
|
+
*
|
|
2196
|
+
* Queries the terminal for the state of DEC private modes.
|
|
2197
|
+
*
|
|
2198
|
+
* Protocol:
|
|
2199
|
+
* - Query: CSI ? {mode} $ p
|
|
2200
|
+
* - Response: CSI ? {mode} ; {Ps} $ y
|
|
2201
|
+
*
|
|
2202
|
+
* Where Ps is:
|
|
2203
|
+
* 1 = set (mode is enabled)
|
|
2204
|
+
* 2 = reset (mode is disabled)
|
|
2205
|
+
* 0 = not recognized (unknown mode)
|
|
2206
|
+
* 3 = permanently set
|
|
2207
|
+
* 4 = permanently reset
|
|
2208
|
+
*
|
|
2209
|
+
* We normalize 3→"set" and 4→"reset" for simplicity.
|
|
2210
|
+
*
|
|
2211
|
+
* Supported by: xterm, Ghostty, Kitty, WezTerm, foot, VTE-based terminals
|
|
2212
|
+
*/
|
|
2213
|
+
/** Regex for DECRPM response: CSI ? mode ; Ps $ y */
|
|
2214
|
+
const DECRPM_RESPONSE_RE = /\x1b\[\?(\d+);(\d+)\$y/;
|
|
2215
|
+
/** Well-known DEC private mode constants. */
|
|
2216
|
+
const DecMode = {
|
|
2217
|
+
CURSOR_VISIBLE: 25,
|
|
2218
|
+
ALT_SCREEN: 1049,
|
|
2219
|
+
MOUSE_TRACKING: 1e3,
|
|
2220
|
+
BRACKETED_PASTE: 2004,
|
|
2221
|
+
SYNC_OUTPUT: 2026,
|
|
2222
|
+
FOCUS_REPORTING: 1004
|
|
2223
|
+
};
|
|
2224
|
+
/**
|
|
2225
|
+
* Query the state of a single DEC private mode.
|
|
2226
|
+
*
|
|
2227
|
+
* @param write Function to write to stdout
|
|
2228
|
+
* @param read Function to read a chunk from stdin
|
|
2229
|
+
* @param mode DEC private mode number (e.g., DecMode.ALT_SCREEN)
|
|
2230
|
+
* @param timeoutMs How long to wait for response (default: 200ms)
|
|
2231
|
+
* @returns "set", "reset", or "unknown"
|
|
2232
|
+
*/
|
|
2233
|
+
async function queryMode(write, read, mode, timeoutMs = 200) {
|
|
2234
|
+
write(`\x1b[?${mode}$p`);
|
|
2235
|
+
const data = await read(timeoutMs);
|
|
2236
|
+
if (data == null) return "unknown";
|
|
2237
|
+
const match = DECRPM_RESPONSE_RE.exec(data);
|
|
2238
|
+
if (!match) return "unknown";
|
|
2239
|
+
if (parseInt(match[1], 10) !== mode) return "unknown";
|
|
2240
|
+
switch (parseInt(match[2], 10)) {
|
|
2241
|
+
case 1:
|
|
2242
|
+
case 3: return "set";
|
|
2243
|
+
case 2:
|
|
2244
|
+
case 4: return "reset";
|
|
2245
|
+
default: return "unknown";
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
/**
|
|
2249
|
+
* Query the state of multiple DEC private modes.
|
|
2250
|
+
*
|
|
2251
|
+
* Queries each mode sequentially and returns a Map of results.
|
|
2252
|
+
*
|
|
2253
|
+
* @param write Function to write to stdout
|
|
2254
|
+
* @param read Function to read a chunk from stdin
|
|
2255
|
+
* @param modes Array of DEC private mode numbers
|
|
2256
|
+
* @param timeoutMs Per-query timeout (default: 200ms)
|
|
2257
|
+
*/
|
|
2258
|
+
async function queryModes(write, read, modes, timeoutMs = 200) {
|
|
2259
|
+
const results = /* @__PURE__ */ new Map();
|
|
2260
|
+
for (const mode of modes) {
|
|
2261
|
+
const state = await queryMode(write, read, mode, timeoutMs);
|
|
2262
|
+
results.set(mode, state);
|
|
2263
|
+
}
|
|
2264
|
+
return results;
|
|
2265
|
+
}
|
|
2266
|
+
//#endregion
|
|
2267
|
+
//#region packages/ag-term/src/pixel-size.ts
|
|
2268
|
+
/**
|
|
2269
|
+
* CSI 14t/18t — Terminal Pixel and Text Area Size Queries
|
|
2270
|
+
*
|
|
2271
|
+
* Queries the terminal for window dimensions in pixels and characters.
|
|
2272
|
+
*
|
|
2273
|
+
* Protocols:
|
|
2274
|
+
*
|
|
2275
|
+
* Text Area Pixels (CSI 14t):
|
|
2276
|
+
* Query: CSI 14 t
|
|
2277
|
+
* Response: CSI 4 ; height ; width t
|
|
2278
|
+
*
|
|
2279
|
+
* Text Area Size in Characters (CSI 18t):
|
|
2280
|
+
* Query: CSI 18 t
|
|
2281
|
+
* Response: CSI 8 ; rows ; cols t
|
|
2282
|
+
*
|
|
2283
|
+
* Cell size can be derived by dividing pixel dimensions by character dimensions.
|
|
2284
|
+
*
|
|
2285
|
+
* Supported by: xterm, Ghostty, Kitty, WezTerm, foot, iTerm2
|
|
2286
|
+
*/
|
|
2287
|
+
/** Regex for CSI 4 ; height ; width t (pixel size response) */
|
|
2288
|
+
const PIXEL_RESPONSE_RE = /\x1b\[4;(\d+);(\d+)t/;
|
|
2289
|
+
/** Regex for CSI 8 ; rows ; cols t (text area size response) */
|
|
2290
|
+
const TEXT_AREA_RESPONSE_RE = /\x1b\[8;(\d+);(\d+)t/;
|
|
2291
|
+
/**
|
|
2292
|
+
* Query the terminal text area size in pixels.
|
|
2293
|
+
*
|
|
2294
|
+
* @param write Function to write to stdout
|
|
2295
|
+
* @param read Function to read a chunk from stdin
|
|
2296
|
+
* @param timeoutMs How long to wait for response (default: 200ms)
|
|
2297
|
+
* @returns Width and height in pixels, or null on timeout/unsupported
|
|
2298
|
+
*/
|
|
2299
|
+
async function queryTextAreaPixels(write, read, timeoutMs = 200) {
|
|
2300
|
+
write("\x1B[14t");
|
|
2301
|
+
const data = await read(timeoutMs);
|
|
2302
|
+
if (data == null) return null;
|
|
2303
|
+
const match = PIXEL_RESPONSE_RE.exec(data);
|
|
2304
|
+
if (!match) return null;
|
|
2305
|
+
return {
|
|
2306
|
+
height: parseInt(match[1], 10),
|
|
2307
|
+
width: parseInt(match[2], 10)
|
|
2308
|
+
};
|
|
2309
|
+
}
|
|
2310
|
+
/**
|
|
2311
|
+
* Query the terminal text area size in characters (rows x columns).
|
|
2312
|
+
*
|
|
2313
|
+
* @param write Function to write to stdout
|
|
2314
|
+
* @param read Function to read a chunk from stdin
|
|
2315
|
+
* @param timeoutMs How long to wait for response (default: 200ms)
|
|
2316
|
+
* @returns Rows and columns, or null on timeout/unsupported
|
|
2317
|
+
*/
|
|
2318
|
+
async function queryTextAreaSize(write, read, timeoutMs = 200) {
|
|
2319
|
+
write("\x1B[18t");
|
|
2320
|
+
const data = await read(timeoutMs);
|
|
2321
|
+
if (data == null) return null;
|
|
2322
|
+
const match = TEXT_AREA_RESPONSE_RE.exec(data);
|
|
2323
|
+
if (!match) return null;
|
|
2324
|
+
return {
|
|
2325
|
+
rows: parseInt(match[1], 10),
|
|
2326
|
+
cols: parseInt(match[2], 10)
|
|
2327
|
+
};
|
|
2328
|
+
}
|
|
2329
|
+
/**
|
|
2330
|
+
* Query the terminal cell size in pixels by querying both pixel
|
|
2331
|
+
* dimensions and character dimensions, then dividing.
|
|
2332
|
+
*
|
|
2333
|
+
* @param write Function to write to stdout
|
|
2334
|
+
* @param read Function to read a chunk from stdin
|
|
2335
|
+
* @param timeoutMs Per-query timeout (default: 200ms)
|
|
2336
|
+
* @returns Cell width and height in pixels, or null if either query fails
|
|
2337
|
+
*/
|
|
2338
|
+
async function queryCellSize(write, read, timeoutMs = 200) {
|
|
2339
|
+
const pixels = await queryTextAreaPixels(write, read, timeoutMs);
|
|
2340
|
+
if (pixels == null) return null;
|
|
2341
|
+
const size = await queryTextAreaSize(write, read, timeoutMs);
|
|
2342
|
+
if (size == null) return null;
|
|
2343
|
+
if (size.cols === 0 || size.rows === 0) return null;
|
|
2344
|
+
return {
|
|
2345
|
+
width: pixels.width / size.cols,
|
|
2346
|
+
height: pixels.height / size.rows
|
|
2347
|
+
};
|
|
2348
|
+
}
|
|
2349
|
+
//#endregion
|
|
2350
|
+
//#region packages/ag-term/src/term-def.ts
|
|
2351
|
+
/**
|
|
2352
|
+
* Default dimensions when not detectable.
|
|
2353
|
+
*/
|
|
2354
|
+
const DEFAULT_WIDTH = 80;
|
|
2355
|
+
const DEFAULT_HEIGHT = 24;
|
|
2356
|
+
/**
|
|
2357
|
+
* Check if a value is a Term instance (duck typing).
|
|
2358
|
+
*
|
|
2359
|
+
* Post km-silvery.caps-restructure Phase 7: the legacy hasCursor/hasInput/
|
|
2360
|
+
* hasColor methods were removed — caps/profile/write/size are the canonical
|
|
2361
|
+
* Term surface.
|
|
2362
|
+
*/
|
|
2363
|
+
function isTerm(value) {
|
|
2364
|
+
if (!value || typeof value !== "object" && typeof value !== "function") return false;
|
|
2365
|
+
const obj = value;
|
|
2366
|
+
return typeof obj.caps === "object" && obj.caps !== null && typeof obj.write === "function" && typeof obj.size === "object";
|
|
2367
|
+
}
|
|
2368
|
+
/**
|
|
2369
|
+
* Check if a value is a TermDef (not a Term).
|
|
2370
|
+
*/
|
|
2371
|
+
function isTermDef(value) {
|
|
2372
|
+
if (!value || typeof value !== "object") return false;
|
|
2373
|
+
return value.caps === void 0;
|
|
2374
|
+
}
|
|
2375
|
+
/**
|
|
2376
|
+
* Resolve a TermDef into concrete values.
|
|
2377
|
+
*
|
|
2378
|
+
* @param def - TermDef to resolve
|
|
2379
|
+
* @returns Resolved values ready for rendering
|
|
2380
|
+
*/
|
|
2381
|
+
function resolveTermDef(def) {
|
|
2382
|
+
const width = def.width ?? def.stdout?.columns ?? DEFAULT_WIDTH;
|
|
2383
|
+
const height = def.height ?? def.stdout?.rows ?? DEFAULT_HEIGHT;
|
|
2384
|
+
let colors;
|
|
2385
|
+
if (def.colors === true) colors = detectColorLevel(def.stdout);
|
|
2386
|
+
else if (def.colors === false || def.colors === null) colors = "mono";
|
|
2387
|
+
else if (def.colors) colors = def.colors;
|
|
2388
|
+
else colors = detectColorLevel(def.stdout);
|
|
2389
|
+
let events = null;
|
|
2390
|
+
if (def.events) events = def.events;
|
|
2391
|
+
else if (def.stdin) events = createInputEvents(def.stdin);
|
|
2392
|
+
return {
|
|
2393
|
+
stdout: def.stdout ?? null,
|
|
2394
|
+
width,
|
|
2395
|
+
height,
|
|
2396
|
+
colors,
|
|
2397
|
+
events,
|
|
2398
|
+
isStatic: events === null
|
|
2399
|
+
};
|
|
2400
|
+
}
|
|
2401
|
+
/**
|
|
2402
|
+
* Resolve a Term instance into ResolvedTermDef.
|
|
2403
|
+
*
|
|
2404
|
+
* @param term - Term instance
|
|
2405
|
+
* @returns Resolved values
|
|
2406
|
+
*/
|
|
2407
|
+
function resolveFromTerm(term) {
|
|
2408
|
+
const { stdin, stdout } = getInternalStreams(term);
|
|
2409
|
+
return {
|
|
2410
|
+
stdout,
|
|
2411
|
+
width: term.cols ?? DEFAULT_WIDTH,
|
|
2412
|
+
height: term.rows ?? DEFAULT_HEIGHT,
|
|
2413
|
+
colors: term.profile.colorLevel,
|
|
2414
|
+
events: createInputEvents(stdin),
|
|
2415
|
+
isStatic: false
|
|
2416
|
+
};
|
|
2417
|
+
}
|
|
2418
|
+
/**
|
|
2419
|
+
* Detect the color tier from a stdout stream.
|
|
2420
|
+
*
|
|
2421
|
+
* Phase 3 of km-silvery.terminal-profile-plateau: this is now a single-line
|
|
2422
|
+
* delegate into `createTerminalProfile` — no more local detection copy.
|
|
2423
|
+
*/
|
|
2424
|
+
function detectColorLevel(stdout) {
|
|
2425
|
+
return createTerminalProfile({ stdout: stdout ?? { isTTY: false } }).colorLevel;
|
|
2426
|
+
}
|
|
2427
|
+
/**
|
|
2428
|
+
* Create an async iterable of input events from a stdin stream.
|
|
2429
|
+
*
|
|
2430
|
+
* This enables interactive mode by providing a source of keyboard events.
|
|
2431
|
+
*/
|
|
2432
|
+
function createInputEvents(stdin) {
|
|
2433
|
+
return { [Symbol.asyncIterator]() {
|
|
2434
|
+
const buffer = [];
|
|
2435
|
+
let resolveNext = null;
|
|
2436
|
+
let done = false;
|
|
2437
|
+
const handleData = (chunk) => {
|
|
2438
|
+
const data = typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
2439
|
+
for (const char of data) {
|
|
2440
|
+
const event = {
|
|
2441
|
+
type: "key",
|
|
2442
|
+
key: char,
|
|
2443
|
+
ctrl: char.charCodeAt(0) < 32 && char !== "\r" && char !== "\n" && char !== " "
|
|
2444
|
+
};
|
|
2445
|
+
if (resolveNext) {
|
|
2446
|
+
resolveNext({
|
|
2447
|
+
value: event,
|
|
2448
|
+
done: false
|
|
2449
|
+
});
|
|
2450
|
+
resolveNext = null;
|
|
2451
|
+
} else buffer.push(event);
|
|
2452
|
+
}
|
|
2453
|
+
};
|
|
2454
|
+
const handleEnd = () => {
|
|
2455
|
+
done = true;
|
|
2456
|
+
if (resolveNext) {
|
|
2457
|
+
resolveNext({
|
|
2458
|
+
value: void 0,
|
|
2459
|
+
done: true
|
|
2460
|
+
});
|
|
2461
|
+
resolveNext = null;
|
|
2462
|
+
}
|
|
2463
|
+
};
|
|
2464
|
+
if (stdin.isTTY && typeof stdin.setRawMode === "function") {
|
|
2465
|
+
stdin.setEncoding("utf8");
|
|
2466
|
+
stdin.on("data", handleData);
|
|
2467
|
+
stdin.on("end", handleEnd);
|
|
2468
|
+
}
|
|
2469
|
+
return {
|
|
2470
|
+
next() {
|
|
2471
|
+
const buffered = buffer.shift();
|
|
2472
|
+
if (buffered) return Promise.resolve({
|
|
2473
|
+
value: buffered,
|
|
2474
|
+
done: false
|
|
2475
|
+
});
|
|
2476
|
+
if (done) return Promise.resolve({
|
|
2477
|
+
value: void 0,
|
|
2478
|
+
done: true
|
|
2479
|
+
});
|
|
2480
|
+
return new Promise((resolve) => {
|
|
2481
|
+
resolveNext = resolve;
|
|
2482
|
+
});
|
|
2483
|
+
},
|
|
2484
|
+
return() {
|
|
2485
|
+
done = true;
|
|
2486
|
+
stdin.off("data", handleData);
|
|
2487
|
+
stdin.off("end", handleEnd);
|
|
2488
|
+
return Promise.resolve({
|
|
2489
|
+
value: void 0,
|
|
2490
|
+
done: true
|
|
2491
|
+
});
|
|
2492
|
+
}
|
|
2493
|
+
};
|
|
2494
|
+
} };
|
|
2495
|
+
}
|
|
2496
|
+
//#endregion
|
|
2497
|
+
//#region packages/ag-term/src/hit-registry-core.ts
|
|
2498
|
+
/**
|
|
2499
|
+
* Registry for managing hit regions.
|
|
2500
|
+
*
|
|
2501
|
+
* Components register their screen regions with targets, and the registry
|
|
2502
|
+
* resolves mouse clicks to the appropriate target based on position and z-index.
|
|
2503
|
+
*
|
|
2504
|
+
* @example
|
|
2505
|
+
* ```typescript
|
|
2506
|
+
* const registry = new HitRegistry();
|
|
2507
|
+
*
|
|
2508
|
+
* // Register a card region
|
|
2509
|
+
* registry.register('card-1', {
|
|
2510
|
+
* x: 10, y: 5, width: 30, height: 8,
|
|
2511
|
+
* target: { type: 'node', nodeId: 'abc123' },
|
|
2512
|
+
* zIndex: 10
|
|
2513
|
+
* });
|
|
2514
|
+
*
|
|
2515
|
+
* // Hit test a click
|
|
2516
|
+
* const target = registry.hitTest(15, 7);
|
|
2517
|
+
* // Returns { type: 'node', nodeId: 'abc123' }
|
|
2518
|
+
* ```
|
|
2519
|
+
*/
|
|
2520
|
+
var HitRegistry = class {
|
|
2521
|
+
regions = /* @__PURE__ */ new Map();
|
|
2522
|
+
/**
|
|
2523
|
+
* Register a hit region with a unique ID.
|
|
2524
|
+
*
|
|
2525
|
+
* @param id - Unique identifier for the region (used for unregistration)
|
|
2526
|
+
* @param region - The region definition including position, size, target, and z-index
|
|
2527
|
+
*/
|
|
2528
|
+
register(id, region) {
|
|
2529
|
+
this.regions.set(id, region);
|
|
2530
|
+
}
|
|
2531
|
+
/**
|
|
2532
|
+
* Unregister a hit region by ID.
|
|
2533
|
+
*
|
|
2534
|
+
* @param id - The ID used when registering the region
|
|
2535
|
+
*/
|
|
2536
|
+
unregister(id) {
|
|
2537
|
+
this.regions.delete(id);
|
|
2538
|
+
}
|
|
2539
|
+
/**
|
|
2540
|
+
* Clear all registered regions.
|
|
2541
|
+
* Useful when the UI is completely redrawn.
|
|
2542
|
+
*/
|
|
2543
|
+
clear() {
|
|
2544
|
+
this.regions.clear();
|
|
2545
|
+
}
|
|
2546
|
+
/**
|
|
2547
|
+
* Get the number of registered regions.
|
|
2548
|
+
* Useful for debugging.
|
|
2549
|
+
*/
|
|
2550
|
+
get size() {
|
|
2551
|
+
return this.regions.size;
|
|
2552
|
+
}
|
|
2553
|
+
/**
|
|
2554
|
+
* Test a screen position and return the highest z-index matching target.
|
|
2555
|
+
*
|
|
2556
|
+
* @param screenX - X position on screen (0-indexed column)
|
|
2557
|
+
* @param screenY - Y position on screen (0-indexed row)
|
|
2558
|
+
* @returns The target of the highest z-index region containing the point, or null if none
|
|
2559
|
+
*/
|
|
2560
|
+
hitTest(screenX, screenY) {
|
|
2561
|
+
let bestMatch = null;
|
|
2562
|
+
for (const region of this.regions.values()) if (screenX >= region.x && screenX < region.x + region.width && screenY >= region.y && screenY < region.y + region.height) {
|
|
2563
|
+
if (!bestMatch || region.zIndex > bestMatch.zIndex) bestMatch = region;
|
|
2564
|
+
}
|
|
2565
|
+
return bestMatch?.target ?? null;
|
|
2566
|
+
}
|
|
2567
|
+
/**
|
|
2568
|
+
* Get all regions that contain a point, sorted by z-index (highest first).
|
|
2569
|
+
* Useful for debugging or when you need to know all overlapping elements.
|
|
2570
|
+
*
|
|
2571
|
+
* @param screenX - X position on screen (0-indexed column)
|
|
2572
|
+
* @param screenY - Y position on screen (0-indexed row)
|
|
2573
|
+
* @returns Array of matching regions, sorted by z-index descending
|
|
2574
|
+
*/
|
|
2575
|
+
hitTestAll(screenX, screenY) {
|
|
2576
|
+
const matches = [];
|
|
2577
|
+
for (const region of this.regions.values()) if (screenX >= region.x && screenX < region.x + region.width && screenY >= region.y && screenY < region.y + region.height) matches.push(region);
|
|
2578
|
+
return matches.sort((a, b) => b.zIndex - a.zIndex);
|
|
2579
|
+
}
|
|
2580
|
+
/**
|
|
2581
|
+
* Debug helper: get all registered regions.
|
|
2582
|
+
*/
|
|
2583
|
+
getAllRegions() {
|
|
2584
|
+
return new Map(this.regions);
|
|
2585
|
+
}
|
|
2586
|
+
};
|
|
2587
|
+
/**
|
|
2588
|
+
* Generate a unique ID for hit region registration.
|
|
2589
|
+
*/
|
|
2590
|
+
let hitRegionIdCounter = 0;
|
|
2591
|
+
function generateHitRegionId() {
|
|
2592
|
+
return `hit-${++hitRegionIdCounter}`;
|
|
2593
|
+
}
|
|
2594
|
+
/**
|
|
2595
|
+
* Reset the ID counter (useful for testing).
|
|
2596
|
+
*/
|
|
2597
|
+
function resetHitRegionIdCounter() {
|
|
2598
|
+
hitRegionIdCounter = 0;
|
|
2599
|
+
}
|
|
2600
|
+
/**
|
|
2601
|
+
* Recommended z-index values for different UI layers.
|
|
2602
|
+
*/
|
|
2603
|
+
const Z_INDEX = {
|
|
2604
|
+
BACKGROUND: 0,
|
|
2605
|
+
COLUMN_HEADER: 5,
|
|
2606
|
+
CARD: 10,
|
|
2607
|
+
FOLD_TOGGLE: 15,
|
|
2608
|
+
LINK: 20,
|
|
2609
|
+
FLOATING: 50,
|
|
2610
|
+
DIALOG: 100,
|
|
2611
|
+
DROPDOWN: 150,
|
|
2612
|
+
TOOLTIP: 200
|
|
2613
|
+
};
|
|
2614
|
+
//#endregion
|
|
2615
|
+
//#region packages/ag-term/src/non-tty.ts
|
|
2616
|
+
/**
|
|
2617
|
+
* Non-TTY Mode Support for Silvery
|
|
2618
|
+
*
|
|
2619
|
+
* Provides detection and rendering modes for non-interactive environments:
|
|
2620
|
+
* - Piped output (process.stdout.isTTY === false)
|
|
2621
|
+
* - CI environments
|
|
2622
|
+
* - TERM=dumb
|
|
2623
|
+
*
|
|
2624
|
+
* When in non-TTY mode, silvery avoids cursor positioning codes that garble
|
|
2625
|
+
* output in non-interactive environments.
|
|
2626
|
+
*
|
|
2627
|
+
* Modes:
|
|
2628
|
+
* - 'auto': Detect based on environment (default)
|
|
2629
|
+
* - 'tty': Force TTY mode (normal cursor positioning)
|
|
2630
|
+
* - 'line-by-line': Simple newline-separated output, no cursor movement
|
|
2631
|
+
* - 'static': Single output at end (no updates)
|
|
2632
|
+
* - 'plain': Strip all ANSI codes
|
|
2633
|
+
*/
|
|
2634
|
+
/**
|
|
2635
|
+
* Check if the environment is a TTY.
|
|
2636
|
+
*
|
|
2637
|
+
* Returns false if:
|
|
2638
|
+
* - stdout.isTTY is false or undefined
|
|
2639
|
+
* - TERM=dumb
|
|
2640
|
+
* - CI environment variables are set
|
|
2641
|
+
*
|
|
2642
|
+
* Pass `emulator` (from `term.emulator` or a TerminalEmulator fixture) when
|
|
2643
|
+
* available to avoid a redundant env read. Without it, the TERM=dumb check
|
|
2644
|
+
* delegates to {@link createTerminalProfile} — the canonical single-source-
|
|
2645
|
+
* of-truth entry in `@silvery/ansi/profile`. CI env vars remain read
|
|
2646
|
+
* directly because they are orthogonal to terminal capabilities (a CI
|
|
2647
|
+
* runner's TTY status doesn't describe what the terminal can render).
|
|
2648
|
+
*/
|
|
2649
|
+
function isTTY(stdout = process.stdout, emulator) {
|
|
2650
|
+
if (!stdout.isTTY) return false;
|
|
2651
|
+
if ((emulator?.TERM ?? createTerminalProfile().emulator.TERM) === "dumb") return false;
|
|
2652
|
+
if (process.env.CI || process.env.GITHUB_ACTIONS || process.env.GITLAB_CI || process.env.JENKINS_URL || process.env.BUILDKITE || process.env.CIRCLECI || process.env.TRAVIS) return false;
|
|
2653
|
+
return true;
|
|
2654
|
+
}
|
|
2655
|
+
/**
|
|
2656
|
+
* Resolve the non-TTY mode based on options and environment.
|
|
2657
|
+
*
|
|
2658
|
+
* When mode is 'auto':
|
|
2659
|
+
* - If TTY detected: returns 'tty'
|
|
2660
|
+
* - If non-TTY detected: returns 'line-by-line'
|
|
2661
|
+
*/
|
|
2662
|
+
function resolveNonTTYMode(options = {}) {
|
|
2663
|
+
const { mode = "auto", stdout = process.stdout } = options;
|
|
2664
|
+
if (mode !== "auto") return mode;
|
|
2665
|
+
return isTTY(stdout) ? "tty" : "line-by-line";
|
|
2666
|
+
}
|
|
2667
|
+
/**
|
|
2668
|
+
* Convert buffer output to line-by-line format.
|
|
2669
|
+
*
|
|
2670
|
+
* Instead of using cursor positioning, outputs each line with a simple
|
|
2671
|
+
* carriage return and clear-to-end-of-line sequence.
|
|
2672
|
+
*
|
|
2673
|
+
* @param content The rendered content (may contain ANSI codes but no cursor positioning)
|
|
2674
|
+
* @param prevLineCount Number of lines in the previous frame (for clearing)
|
|
2675
|
+
* @returns Output string suitable for non-TTY rendering
|
|
2676
|
+
*/
|
|
2677
|
+
function toLineByLineOutput(content, prevLineCount) {
|
|
2678
|
+
const lines = content.split("\n");
|
|
2679
|
+
let output = "";
|
|
2680
|
+
if (prevLineCount > 0) {
|
|
2681
|
+
output += "\r";
|
|
2682
|
+
if (prevLineCount > 1) output += `\x1b[${prevLineCount - 1}A`;
|
|
2683
|
+
}
|
|
2684
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2685
|
+
if (i > 0) output += "\n";
|
|
2686
|
+
output += lines[i];
|
|
2687
|
+
output += "\x1B[K";
|
|
2688
|
+
}
|
|
2689
|
+
const extraLines = prevLineCount - lines.length;
|
|
2690
|
+
if (extraLines > 0) {
|
|
2691
|
+
for (let i = 0; i < extraLines; i++) output += "\n\x1B[K";
|
|
2692
|
+
output += `\x1b[${extraLines}A`;
|
|
2693
|
+
}
|
|
2694
|
+
return output;
|
|
2695
|
+
}
|
|
2696
|
+
/**
|
|
2697
|
+
* Convert buffer output to plain text format.
|
|
2698
|
+
*
|
|
2699
|
+
* Strips all ANSI codes and outputs simple newline-separated text.
|
|
2700
|
+
* No cursor movement or clearing.
|
|
2701
|
+
*
|
|
2702
|
+
* @param content The rendered content
|
|
2703
|
+
* @param prevLineCount Number of lines in the previous frame (unused in plain mode)
|
|
2704
|
+
* @returns Plain text output
|
|
2705
|
+
*/
|
|
2706
|
+
function toPlainOutput(content, _prevLineCount) {
|
|
2707
|
+
const lines = stripAnsi(content).split("\n").map((line) => line.trimEnd());
|
|
2708
|
+
while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
|
|
2709
|
+
return lines.join("\n");
|
|
2710
|
+
}
|
|
2711
|
+
/**
|
|
2712
|
+
* Create an output transformer based on the non-TTY mode.
|
|
2713
|
+
*
|
|
2714
|
+
* @param mode The resolved non-TTY mode
|
|
2715
|
+
* @returns A function that transforms output based on the mode
|
|
2716
|
+
*/
|
|
2717
|
+
function createOutputTransformer(mode) {
|
|
2718
|
+
switch (mode) {
|
|
2719
|
+
case "tty": return (content) => content;
|
|
2720
|
+
case "line-by-line": return toLineByLineOutput;
|
|
2721
|
+
case "static": return () => "";
|
|
2722
|
+
case "plain": return toPlainOutput;
|
|
2723
|
+
}
|
|
2724
|
+
}
|
|
2725
|
+
/**
|
|
2726
|
+
* Count the number of lines in a string.
|
|
2727
|
+
*/
|
|
2728
|
+
function countLines(str) {
|
|
2729
|
+
if (!str) return 0;
|
|
2730
|
+
return str.split("\n").length;
|
|
2731
|
+
}
|
|
2732
|
+
//#endregion
|
|
2733
|
+
//#region packages/ag-term/src/inspector.ts
|
|
2734
|
+
let inspectorEnabled = false;
|
|
2735
|
+
let inspectorOutput = process.stderr;
|
|
2736
|
+
/** Enable the silvery inspector. */
|
|
2737
|
+
function enableInspector(options) {
|
|
2738
|
+
inspectorEnabled = true;
|
|
2739
|
+
if (options?.logFile) inspectorOutput = process.getBuiltinModule("node:fs").createWriteStream(options.logFile, { flags: "a" });
|
|
2740
|
+
else if (options?.output) inspectorOutput = options.output;
|
|
2741
|
+
else inspectorOutput = process.stderr;
|
|
2742
|
+
}
|
|
2743
|
+
/** Disable the inspector. */
|
|
2744
|
+
function disableInspector() {
|
|
2745
|
+
inspectorEnabled = false;
|
|
2746
|
+
}
|
|
2747
|
+
/** Check if inspector is active. */
|
|
2748
|
+
function isInspectorEnabled() {
|
|
2749
|
+
return inspectorEnabled;
|
|
2750
|
+
}
|
|
2751
|
+
/**
|
|
2752
|
+
* Log render stats after each frame.
|
|
2753
|
+
*
|
|
2754
|
+
* Called by the scheduler after doRender completes. When the inspector
|
|
2755
|
+
* is disabled this is a no-op (zero overhead).
|
|
2756
|
+
*/
|
|
2757
|
+
function inspectFrame(stats) {
|
|
2758
|
+
if (!inspectorEnabled) return;
|
|
2759
|
+
const line = `[silvery] frame #${stats.renderCount} ${stats.lastRenderTime.toFixed(1)}ms avg=${stats.avgRenderTime.toFixed(1)}ms skipped=${stats.skippedCount}\n`;
|
|
2760
|
+
inspectorOutput.write(line);
|
|
2761
|
+
}
|
|
2762
|
+
/**
|
|
2763
|
+
* Dump the component tree structure as indented text.
|
|
2764
|
+
*
|
|
2765
|
+
* Walks the SilveryNode tree and formats each node with its type, testID,
|
|
2766
|
+
* layout rect, and dirty flags.
|
|
2767
|
+
*/
|
|
2768
|
+
function inspectTree(rootNode, options) {
|
|
2769
|
+
const maxDepth = options?.depth ?? 10;
|
|
2770
|
+
const showLayout = options?.showLayout ?? true;
|
|
2771
|
+
const lines = [];
|
|
2772
|
+
function walk(node, indent) {
|
|
2773
|
+
if (indent > maxDepth) return;
|
|
2774
|
+
const prefix = " ".repeat(indent);
|
|
2775
|
+
const type = node.type;
|
|
2776
|
+
const testID = node.props?.testID;
|
|
2777
|
+
const idStr = testID ? ` #${testID}` : "";
|
|
2778
|
+
let rectStr = "";
|
|
2779
|
+
if (showLayout) {
|
|
2780
|
+
if (node.boxRect) {
|
|
2781
|
+
const r = node.boxRect;
|
|
2782
|
+
rectStr = ` [${r.x},${r.y} ${r.width}x${r.height}]`;
|
|
2783
|
+
} else if (node.layoutNode) {
|
|
2784
|
+
const ln = node.layoutNode;
|
|
2785
|
+
rectStr = ` [${ln.getComputedLeft()},${ln.getComputedTop()} ${ln.getComputedWidth()}x${ln.getComputedHeight()}]`;
|
|
2786
|
+
}
|
|
2787
|
+
}
|
|
2788
|
+
const dirtyFlags = [];
|
|
2789
|
+
if (node.layoutNode?.isDirty()) dirtyFlags.push("layout");
|
|
2790
|
+
if (isDirty(node.dirtyBits, node.dirtyEpoch, 1)) dirtyFlags.push("content");
|
|
2791
|
+
if (isDirty(node.dirtyBits, node.dirtyEpoch, 2)) dirtyFlags.push("paint");
|
|
2792
|
+
if (isDirty(node.dirtyBits, node.dirtyEpoch, 4)) dirtyFlags.push("bg");
|
|
2793
|
+
if (isDirty(node.dirtyBits, node.dirtyEpoch, 16)) dirtyFlags.push("subtree");
|
|
2794
|
+
if (isDirty(node.dirtyBits, node.dirtyEpoch, 8)) dirtyFlags.push("children");
|
|
2795
|
+
const dirtyStr = dirtyFlags.length > 0 ? ` dirty=[${dirtyFlags.join(",")}]` : "";
|
|
2796
|
+
const textStr = node.textContent ? ` "${node.textContent.slice(0, 30)}${node.textContent.length > 30 ? "..." : ""}"` : "";
|
|
2797
|
+
lines.push(`${prefix}${type}${idStr}${rectStr}${dirtyStr}${textStr}`);
|
|
2798
|
+
for (const child of node.children) walk(child, indent + 1);
|
|
2799
|
+
}
|
|
2800
|
+
walk(rootNode, 0);
|
|
2801
|
+
return lines.join("\n");
|
|
2802
|
+
}
|
|
2803
|
+
/**
|
|
2804
|
+
* Auto-enable if SILVERY_DEV=1 is set.
|
|
2805
|
+
*
|
|
2806
|
+
* Call this at startup to respect the environment variable convention.
|
|
2807
|
+
*/
|
|
2808
|
+
function autoEnableInspector() {
|
|
2809
|
+
if (process.env.SILVERY_DEV === "1" || process.env.SILVERY_DEV === "true") {
|
|
2810
|
+
const logFile = process.env.SILVERY_DEV_LOG;
|
|
2811
|
+
enableInspector(logFile ? { logFile } : void 0);
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2814
|
+
//#endregion
|
|
2815
|
+
//#region packages/ag-term/src/measurer.ts
|
|
2816
|
+
/**
|
|
2817
|
+
* Extend a Term with measurement capabilities.
|
|
2818
|
+
*
|
|
2819
|
+
* Creates a width measurer from the term's caps and adds measurement
|
|
2820
|
+
* methods (displayWidth, graphemeWidth, wrapText, etc.) to the term.
|
|
2821
|
+
*/
|
|
2822
|
+
function withMeasurer(term) {
|
|
2823
|
+
const caps = term.caps;
|
|
2824
|
+
const measurer = createMeasurer(caps ?? {});
|
|
2825
|
+
return Object.create(term, {
|
|
2826
|
+
maybeWideEmojis: {
|
|
2827
|
+
get: () => measurer.maybeWideEmojis,
|
|
2828
|
+
enumerable: true
|
|
2829
|
+
},
|
|
2830
|
+
textSizing: {
|
|
2831
|
+
get: () => measurer.textSizing,
|
|
2832
|
+
enumerable: true
|
|
2833
|
+
},
|
|
2834
|
+
displayWidth: {
|
|
2835
|
+
value: measurer.displayWidth.bind(measurer),
|
|
2836
|
+
enumerable: true
|
|
2837
|
+
},
|
|
2838
|
+
displayWidthAnsi: {
|
|
2839
|
+
value: measurer.displayWidthAnsi.bind(measurer),
|
|
2840
|
+
enumerable: true
|
|
2841
|
+
},
|
|
2842
|
+
graphemeWidth: {
|
|
2843
|
+
value: measurer.graphemeWidth.bind(measurer),
|
|
2844
|
+
enumerable: true
|
|
2845
|
+
},
|
|
2846
|
+
wrapText: {
|
|
2847
|
+
value: measurer.wrapText.bind(measurer),
|
|
2848
|
+
enumerable: true
|
|
2849
|
+
},
|
|
2850
|
+
sliceByWidth: {
|
|
2851
|
+
value: measurer.sliceByWidth.bind(measurer),
|
|
2852
|
+
enumerable: true
|
|
2853
|
+
},
|
|
2854
|
+
sliceByWidthFromEnd: {
|
|
2855
|
+
value: measurer.sliceByWidthFromEnd.bind(measurer),
|
|
2856
|
+
enumerable: true
|
|
2857
|
+
}
|
|
2858
|
+
});
|
|
2859
|
+
}
|
|
2860
|
+
/**
|
|
2861
|
+
* Create a pipeline configuration from caps and/or measurer.
|
|
2862
|
+
*
|
|
2863
|
+
* This is the single factory for PipelineConfig -- use it instead of
|
|
2864
|
+
* manually constructing { measurer, outputPhaseFn }.
|
|
2865
|
+
*
|
|
2866
|
+
* @param options.caps - Terminal capabilities (for output phase SGR generation).
|
|
2867
|
+
* Reads `caps.maybeWideEmojis` (text-presentation emoji width) and
|
|
2868
|
+
* `caps.textSizing` for measurement.
|
|
2869
|
+
* @param options.measurer - Explicit measurer (if omitted, created from caps)
|
|
2870
|
+
*/
|
|
2871
|
+
function createPipeline(options = {}) {
|
|
2872
|
+
const { caps, measurer: explicitMeasurer } = options;
|
|
2873
|
+
const measurer = explicitMeasurer ?? createMeasurer(caps ?? {});
|
|
2874
|
+
const outputPhaseFn = createOutputPhase(caps ? {
|
|
2875
|
+
underlineStyles: caps.underlineStyles,
|
|
2876
|
+
underlineColor: caps.underlineColor,
|
|
2877
|
+
overline: caps.overline,
|
|
2878
|
+
colorLevel: caps.colorLevel
|
|
2879
|
+
} : {}, measurer);
|
|
2880
|
+
if (caps?.colorLevel) setActiveColorLevel(caps.colorLevel);
|
|
2881
|
+
return {
|
|
2882
|
+
measurer,
|
|
2883
|
+
outputPhaseFn
|
|
2884
|
+
};
|
|
2885
|
+
}
|
|
2886
|
+
//#endregion
|
|
2887
|
+
//#region packages/ag-term/src/scroll-utils.ts
|
|
2888
|
+
/**
|
|
2889
|
+
* Scroll Utilities
|
|
2890
|
+
*
|
|
2891
|
+
* Shared functions for edge-based scrolling behavior across VirtualList,
|
|
2892
|
+
* HorizontalVirtualList, and other scroll-aware components.
|
|
2893
|
+
*/
|
|
2894
|
+
/**
|
|
2895
|
+
* Calculate edge-based scroll offset.
|
|
2896
|
+
*
|
|
2897
|
+
* Only scrolls when cursor approaches the edge of the visible area.
|
|
2898
|
+
* This provides smoother scrolling by starting to scroll before hitting
|
|
2899
|
+
* the absolute edge, maintaining context around the selected item.
|
|
2900
|
+
*
|
|
2901
|
+
* ## Algorithm
|
|
2902
|
+
*
|
|
2903
|
+
* The viewport is divided into zones:
|
|
2904
|
+
* ```
|
|
2905
|
+
* |<padding>|<------ safe zone ------>|<padding>|
|
|
2906
|
+
* | scroll | no scroll needed | scroll |
|
|
2907
|
+
* | if < | | if > |
|
|
2908
|
+
* ```
|
|
2909
|
+
*
|
|
2910
|
+
* When the selected item enters a padding zone, the viewport scrolls
|
|
2911
|
+
* to keep the item visible with margin.
|
|
2912
|
+
*
|
|
2913
|
+
* ## Asymmetry Note
|
|
2914
|
+
*
|
|
2915
|
+
* The +1 in the "scroll down/right" case is intentional:
|
|
2916
|
+
* - Offset points to the TOP/LEFT of the viewport
|
|
2917
|
+
* - We want the selected item to be `padding` items from the BOTTOM/RIGHT
|
|
2918
|
+
* - Formula: `selectedIndex - visibleCount + padding + 1`
|
|
2919
|
+
*
|
|
2920
|
+
* Example: visibleCount=10, padding=2, selectedIndex=15
|
|
2921
|
+
* offset = 15 - 10 + 2 + 1 = 8
|
|
2922
|
+
* viewport shows items 8-17, selected item 15 is at position 7 (2 from bottom)
|
|
2923
|
+
*
|
|
2924
|
+
* @param selectedIndex - Currently selected item index
|
|
2925
|
+
* @param currentOffset - Current scroll offset (topmost/leftmost visible item)
|
|
2926
|
+
* @param visibleCount - Number of items visible in viewport
|
|
2927
|
+
* @param totalCount - Total number of items
|
|
2928
|
+
* @param padding - Items to keep visible before/after cursor (default: 1)
|
|
2929
|
+
* @returns New scroll offset
|
|
2930
|
+
*/
|
|
2931
|
+
function calcEdgeBasedScrollOffset(selectedIndex, currentOffset, visibleCount, totalCount, padding = 1) {
|
|
2932
|
+
if (totalCount <= visibleCount) return 0;
|
|
2933
|
+
const effectivePadding = padding * 2 >= visibleCount ? 0 : padding;
|
|
2934
|
+
const visibleStart = currentOffset;
|
|
2935
|
+
const visibleEnd = currentOffset + visibleCount - 1;
|
|
2936
|
+
const paddedStart = visibleStart + effectivePadding;
|
|
2937
|
+
const paddedEnd = visibleEnd - effectivePadding;
|
|
2938
|
+
let newOffset = currentOffset;
|
|
2939
|
+
if (selectedIndex < paddedStart) newOffset = Math.max(0, selectedIndex - effectivePadding);
|
|
2940
|
+
else if (effectivePadding === 0 && selectedIndex === paddedStart && currentOffset > 0 && visibleCount > padding) newOffset = Math.max(0, selectedIndex - padding);
|
|
2941
|
+
else if (selectedIndex > paddedEnd) newOffset = Math.min(totalCount - visibleCount, selectedIndex - visibleCount + effectivePadding + 1);
|
|
2942
|
+
return Math.max(0, Math.min(newOffset, totalCount - visibleCount));
|
|
2943
|
+
}
|
|
2944
|
+
//#endregion
|
|
2945
|
+
//#region packages/ag-term/src/scroll-region.ts
|
|
2946
|
+
/**
|
|
2947
|
+
* Terminal scroll region (DECSTBM) utilities.
|
|
2948
|
+
*
|
|
2949
|
+
* Scroll regions tell the terminal to natively scroll content within
|
|
2950
|
+
* a defined row range, which is faster than re-rendering all cells.
|
|
2951
|
+
*
|
|
2952
|
+
* DECSTBM (DEC Set Top and Bottom Margins) is supported by most modern
|
|
2953
|
+
* terminal emulators: xterm, iTerm2, Kitty, Ghostty, WezTerm, etc.
|
|
2954
|
+
*/
|
|
2955
|
+
const ESC = "\x1B";
|
|
2956
|
+
/** Set terminal scroll region (1-indexed top and bottom rows). */
|
|
2957
|
+
function setScrollRegion(stdout, top, bottom) {
|
|
2958
|
+
stdout.write(`${ESC}[${top};${bottom}r`);
|
|
2959
|
+
}
|
|
2960
|
+
/** Reset scroll region to full terminal. */
|
|
2961
|
+
function resetScrollRegion(stdout) {
|
|
2962
|
+
stdout.write(`${ESC}[r`);
|
|
2963
|
+
}
|
|
2964
|
+
/** Scroll content up by N lines within the current scroll region. */
|
|
2965
|
+
function scrollUp(stdout, lines = 1) {
|
|
2966
|
+
stdout.write(`${ESC}[${lines}S`);
|
|
2967
|
+
}
|
|
2968
|
+
/** Scroll content down by N lines within the current scroll region. */
|
|
2969
|
+
function scrollDown(stdout, lines = 1) {
|
|
2970
|
+
stdout.write(`${ESC}[${lines}T`);
|
|
2971
|
+
}
|
|
2972
|
+
/** Move cursor to a specific position (1-indexed row and column). */
|
|
2973
|
+
function moveCursor(stdout, row, col) {
|
|
2974
|
+
stdout.write(`${ESC}[${row};${col}H`);
|
|
2975
|
+
}
|
|
2976
|
+
/**
|
|
2977
|
+
* Detect if the terminal likely supports DECSTBM scroll regions.
|
|
2978
|
+
*
|
|
2979
|
+
* Most modern terminals do (xterm, iTerm2, Kitty, Ghostty, WezTerm, etc.)
|
|
2980
|
+
* but some (e.g., Linux console) may not handle them correctly.
|
|
2981
|
+
*
|
|
2982
|
+
* Pass `emulator` (via `term.emulator` or a test fixture) when in scope.
|
|
2983
|
+
* Without it, this falls back to {@link createTerminalProfile} — the canonical
|
|
2984
|
+
* single-source-of-truth entry point in `@silvery/ansi/profile`. Direct
|
|
2985
|
+
* reads of terminal-signal env vars (TERM / TERM_PROGRAM / …) are banned
|
|
2986
|
+
* outside that module — see `scripts/lint-env-reads.ts`.
|
|
2987
|
+
*/
|
|
2988
|
+
function supportsScrollRegions(emulator) {
|
|
2989
|
+
const resolved = emulator ?? createTerminalProfile().emulator;
|
|
2990
|
+
const term = resolved.TERM;
|
|
2991
|
+
const termProgram = resolved.program;
|
|
2992
|
+
if (termProgram === "iTerm.app" || termProgram === "WezTerm" || termProgram === "ghostty" || termProgram === "Ghostty" || termProgram === "vscode") return true;
|
|
2993
|
+
if (term.includes("xterm") || term.includes("screen") || term.includes("tmux") || term.includes("kitty")) return true;
|
|
2994
|
+
if (term === "linux") return false;
|
|
2995
|
+
return term !== "";
|
|
2996
|
+
}
|
|
2997
|
+
//#endregion
|
|
2998
|
+
//#region packages/ag-term/src/pane-manager.ts
|
|
2999
|
+
/** Create a single-pane layout */
|
|
3000
|
+
function createLeaf(id) {
|
|
3001
|
+
return {
|
|
3002
|
+
type: "leaf",
|
|
3003
|
+
id
|
|
3004
|
+
};
|
|
3005
|
+
}
|
|
3006
|
+
/** Get all leaf pane IDs in depth-first left-to-right order */
|
|
3007
|
+
function getPaneIds(layout) {
|
|
3008
|
+
if (layout.type === "leaf") return [layout.id];
|
|
3009
|
+
return [...getPaneIds(layout.first), ...getPaneIds(layout.second)];
|
|
3010
|
+
}
|
|
3011
|
+
/** Find the next/previous pane in tab order (depth-first left-to-right) */
|
|
3012
|
+
function getTabOrder(layout) {
|
|
3013
|
+
return getPaneIds(layout);
|
|
3014
|
+
}
|
|
3015
|
+
/**
|
|
3016
|
+
* Split a pane into two. Returns new layout tree with the target pane split.
|
|
3017
|
+
* The original pane becomes the first child; the new pane becomes the second.
|
|
3018
|
+
*/
|
|
3019
|
+
function splitPane(layout, targetPaneId, direction, newPaneId, ratio = .5) {
|
|
3020
|
+
const clampedRatio = clampRatio(ratio);
|
|
3021
|
+
if (layout.type === "leaf") {
|
|
3022
|
+
if (layout.id === targetPaneId) return {
|
|
3023
|
+
type: "split",
|
|
3024
|
+
direction,
|
|
3025
|
+
ratio: clampedRatio,
|
|
3026
|
+
first: {
|
|
3027
|
+
type: "leaf",
|
|
3028
|
+
id: targetPaneId
|
|
3029
|
+
},
|
|
3030
|
+
second: {
|
|
3031
|
+
type: "leaf",
|
|
3032
|
+
id: newPaneId
|
|
3033
|
+
}
|
|
3034
|
+
};
|
|
3035
|
+
return layout;
|
|
3036
|
+
}
|
|
3037
|
+
const newFirst = splitPane(layout.first, targetPaneId, direction, newPaneId, ratio);
|
|
3038
|
+
const newSecond = splitPane(layout.second, targetPaneId, direction, newPaneId, ratio);
|
|
3039
|
+
if (newFirst === layout.first && newSecond === layout.second) return layout;
|
|
3040
|
+
return {
|
|
3041
|
+
...layout,
|
|
3042
|
+
first: newFirst,
|
|
3043
|
+
second: newSecond
|
|
3044
|
+
};
|
|
3045
|
+
}
|
|
3046
|
+
/**
|
|
3047
|
+
* Remove a pane from the layout. The sibling takes the full space.
|
|
3048
|
+
* Returns null if the removed pane was the last one.
|
|
3049
|
+
*/
|
|
3050
|
+
function removePane(layout, paneId) {
|
|
3051
|
+
if (layout.type === "leaf") return layout.id === paneId ? null : layout;
|
|
3052
|
+
if (layout.first.type === "leaf" && layout.first.id === paneId) return layout.second;
|
|
3053
|
+
if (layout.second.type === "leaf" && layout.second.id === paneId) return layout.first;
|
|
3054
|
+
const newFirst = removePane(layout.first, paneId);
|
|
3055
|
+
const newSecond = removePane(layout.second, paneId);
|
|
3056
|
+
if (newFirst === null) return newSecond;
|
|
3057
|
+
if (newSecond === null) return newFirst;
|
|
3058
|
+
if (newFirst === layout.first && newSecond === layout.second) return layout;
|
|
3059
|
+
return {
|
|
3060
|
+
...layout,
|
|
3061
|
+
first: newFirst,
|
|
3062
|
+
second: newSecond
|
|
3063
|
+
};
|
|
3064
|
+
}
|
|
3065
|
+
/** Swap two panes' positions in the layout */
|
|
3066
|
+
function swapPanes(layout, paneId1, paneId2) {
|
|
3067
|
+
if (layout.type === "leaf") {
|
|
3068
|
+
if (layout.id === paneId1) return {
|
|
3069
|
+
type: "leaf",
|
|
3070
|
+
id: paneId2
|
|
3071
|
+
};
|
|
3072
|
+
if (layout.id === paneId2) return {
|
|
3073
|
+
type: "leaf",
|
|
3074
|
+
id: paneId1
|
|
3075
|
+
};
|
|
3076
|
+
return layout;
|
|
3077
|
+
}
|
|
3078
|
+
const newFirst = swapPanes(layout.first, paneId1, paneId2);
|
|
3079
|
+
const newSecond = swapPanes(layout.second, paneId1, paneId2);
|
|
3080
|
+
if (newFirst === layout.first && newSecond === layout.second) return layout;
|
|
3081
|
+
return {
|
|
3082
|
+
...layout,
|
|
3083
|
+
first: newFirst,
|
|
3084
|
+
second: newSecond
|
|
3085
|
+
};
|
|
3086
|
+
}
|
|
3087
|
+
/**
|
|
3088
|
+
* Resize a split: adjust the ratio of the nearest ancestor split
|
|
3089
|
+
* that contains the target pane as its first child.
|
|
3090
|
+
* Positive delta = grow (first child gets more), negative = shrink.
|
|
3091
|
+
*/
|
|
3092
|
+
function resizeSplit(layout, paneId, delta) {
|
|
3093
|
+
if (layout.type === "leaf") return layout;
|
|
3094
|
+
if (getPaneIds(layout.first).includes(paneId)) {
|
|
3095
|
+
const newRatio = clampRatio(layout.ratio + delta);
|
|
3096
|
+
if (newRatio === layout.ratio) return layout;
|
|
3097
|
+
return {
|
|
3098
|
+
...layout,
|
|
3099
|
+
ratio: newRatio
|
|
3100
|
+
};
|
|
3101
|
+
}
|
|
3102
|
+
if (getPaneIds(layout.second).includes(paneId)) {
|
|
3103
|
+
const newRatio = clampRatio(layout.ratio - delta);
|
|
3104
|
+
if (newRatio === layout.ratio) return layout;
|
|
3105
|
+
return {
|
|
3106
|
+
...layout,
|
|
3107
|
+
ratio: newRatio
|
|
3108
|
+
};
|
|
3109
|
+
}
|
|
3110
|
+
return layout;
|
|
3111
|
+
}
|
|
3112
|
+
/**
|
|
3113
|
+
* Find the pane adjacent to the given pane in a direction.
|
|
3114
|
+
*
|
|
3115
|
+
* For left/right: looks for siblings in horizontal splits.
|
|
3116
|
+
* For up/down: looks for siblings in vertical splits.
|
|
3117
|
+
*
|
|
3118
|
+
* Returns null if no adjacent pane exists in that direction.
|
|
3119
|
+
*/
|
|
3120
|
+
function findAdjacentPane(layout, paneId, direction) {
|
|
3121
|
+
const path = findPath(layout, paneId);
|
|
3122
|
+
if (!path) return null;
|
|
3123
|
+
const splitDirection = direction === "left" || direction === "right" ? "horizontal" : "vertical";
|
|
3124
|
+
const goToSecond = direction === "right" || direction === "down";
|
|
3125
|
+
for (let i = path.length - 1; i >= 0; i--) {
|
|
3126
|
+
const step = path[i];
|
|
3127
|
+
if (step.node.type !== "split") continue;
|
|
3128
|
+
if (step.node.direction !== splitDirection) continue;
|
|
3129
|
+
if (step.side === "first" && goToSecond) return firstLeaf(step.node.second);
|
|
3130
|
+
if (step.side === "second" && !goToSecond) return lastLeaf(step.node.first);
|
|
3131
|
+
}
|
|
3132
|
+
return null;
|
|
3133
|
+
}
|
|
3134
|
+
function clampRatio(ratio) {
|
|
3135
|
+
return Math.max(.1, Math.min(.9, ratio));
|
|
3136
|
+
}
|
|
3137
|
+
/** Get the first (leftmost/topmost) leaf ID */
|
|
3138
|
+
function firstLeaf(node) {
|
|
3139
|
+
if (node.type === "leaf") return node.id;
|
|
3140
|
+
return firstLeaf(node.first);
|
|
3141
|
+
}
|
|
3142
|
+
/** Get the last (rightmost/bottommost) leaf ID */
|
|
3143
|
+
function lastLeaf(node) {
|
|
3144
|
+
if (node.type === "leaf") return node.id;
|
|
3145
|
+
return lastLeaf(node.second);
|
|
3146
|
+
}
|
|
3147
|
+
/** Find the path from root to a leaf, recording which side we took at each split */
|
|
3148
|
+
function findPath(layout, paneId) {
|
|
3149
|
+
if (layout.type === "leaf") return layout.id === paneId ? [] : null;
|
|
3150
|
+
const firstPath = findPath(layout.first, paneId);
|
|
3151
|
+
if (firstPath !== null) return [{
|
|
3152
|
+
node: layout,
|
|
3153
|
+
side: "first"
|
|
3154
|
+
}, ...firstPath];
|
|
3155
|
+
const secondPath = findPath(layout.second, paneId);
|
|
3156
|
+
if (secondPath !== null) return [{
|
|
3157
|
+
node: layout,
|
|
3158
|
+
side: "second"
|
|
3159
|
+
}, ...secondPath];
|
|
3160
|
+
return null;
|
|
3161
|
+
}
|
|
3162
|
+
//#endregion
|
|
3163
|
+
//#region packages/ag-term/src/selection-renderer.ts
|
|
3164
|
+
function normalizeSelectionColor(color) {
|
|
3165
|
+
if (color === void 0) return void 0;
|
|
3166
|
+
if (typeof color === "string") return parseColor(color);
|
|
3167
|
+
return color;
|
|
3168
|
+
}
|
|
3169
|
+
/**
|
|
3170
|
+
* Compute selection style changes for all cells within a selection range.
|
|
3171
|
+
*
|
|
3172
|
+
* Returns a sparse list of cell changes (fg/bg only). The caller applies these
|
|
3173
|
+
* to the buffer before the output phase diffs it. This approach:
|
|
3174
|
+
* - Composes correctly with existing cell styles
|
|
3175
|
+
* - Handles already-inverted content (swaps back to normal instead of double-inverting)
|
|
3176
|
+
* - Respects SELECTABLE_FLAG per cell (skip non-selectable)
|
|
3177
|
+
* - Works with the normal diff/output renderer (no separate ANSI pass)
|
|
3178
|
+
*
|
|
3179
|
+
* @param buffer The rendered buffer (post-render-phase)
|
|
3180
|
+
* @param selection Current selection range, or null
|
|
3181
|
+
* @param theme Optional selection theme colors
|
|
3182
|
+
* @param respectSelectableFlag When true, skip cells without SELECTABLE_FLAG
|
|
3183
|
+
*/
|
|
3184
|
+
function composeSelectionCells(buffer, selection, theme, respectSelectableFlag = false, scope) {
|
|
3185
|
+
if (!selection) return [];
|
|
3186
|
+
const { startRow, startCol, endRow, endCol } = normalizeRange(selection);
|
|
3187
|
+
const changes = [];
|
|
3188
|
+
for (let row = startRow; row <= endRow; row++) {
|
|
3189
|
+
let colStart = row === startRow ? startCol : 0;
|
|
3190
|
+
let colEnd = row === endRow ? endCol : buffer.width - 1;
|
|
3191
|
+
const rowMeta = buffer.getRowMeta(row);
|
|
3192
|
+
if (rowMeta.lastContentCol >= 0) colEnd = Math.min(colEnd, rowMeta.lastContentCol);
|
|
3193
|
+
if (colStart > colEnd) continue;
|
|
3194
|
+
if (scope) {
|
|
3195
|
+
colStart = Math.max(colStart, scope.left);
|
|
3196
|
+
colEnd = Math.min(colEnd, scope.right);
|
|
3197
|
+
if (colStart > colEnd) continue;
|
|
3198
|
+
}
|
|
3199
|
+
const firstContentCol = findFirstContentCol(buffer, row, colStart, colEnd, respectSelectableFlag);
|
|
3200
|
+
if (firstContentCol === null) continue;
|
|
3201
|
+
colStart = Math.max(colStart, firstContentCol);
|
|
3202
|
+
for (let col = colStart; col <= colEnd; col++) {
|
|
3203
|
+
if (buffer.isCellContinuation(col, row)) continue;
|
|
3204
|
+
if (respectSelectableFlag && !buffer.isCellSelectable(col, row)) continue;
|
|
3205
|
+
const cellFg = buffer.getCellFg(col, row);
|
|
3206
|
+
const cellBg = buffer.getCellBg(col, row);
|
|
3207
|
+
let newFg;
|
|
3208
|
+
let newBg;
|
|
3209
|
+
let inverseAttr = false;
|
|
3210
|
+
if (theme?.selectionBg != null) {
|
|
3211
|
+
newFg = theme.selectionFg ?? cellFg;
|
|
3212
|
+
newBg = theme.selectionBg;
|
|
3213
|
+
} else if (cellFg == null || cellBg == null) {
|
|
3214
|
+
newFg = cellFg;
|
|
3215
|
+
newBg = cellBg;
|
|
3216
|
+
inverseAttr = true;
|
|
3217
|
+
} else {
|
|
3218
|
+
newFg = cellBg;
|
|
3219
|
+
newBg = cellFg;
|
|
3220
|
+
}
|
|
3221
|
+
changes.push({
|
|
3222
|
+
col,
|
|
3223
|
+
row,
|
|
3224
|
+
fg: newFg,
|
|
3225
|
+
bg: newBg,
|
|
3226
|
+
inverseAttr
|
|
3227
|
+
});
|
|
3228
|
+
}
|
|
3229
|
+
}
|
|
3230
|
+
return changes;
|
|
3231
|
+
}
|
|
3232
|
+
function findFirstContentCol(buffer, row, colStart, colEnd, respectSelectableFlag) {
|
|
3233
|
+
for (let col = colStart; col <= colEnd; col++) {
|
|
3234
|
+
if (buffer.isCellContinuation(col, row)) continue;
|
|
3235
|
+
if (respectSelectableFlag && !buffer.isCellSelectable(col, row)) continue;
|
|
3236
|
+
if (buffer.getCell(col, row).char.trim().length > 0) return col;
|
|
3237
|
+
}
|
|
3238
|
+
return null;
|
|
3239
|
+
}
|
|
3240
|
+
/**
|
|
3241
|
+
* Apply selection style changes to a buffer.
|
|
3242
|
+
*
|
|
3243
|
+
* Modifies the buffer in-place by setting fg/bg on affected cells.
|
|
3244
|
+
* Call this after the render phase and before the output phase.
|
|
3245
|
+
*
|
|
3246
|
+
* @param buffer The rendered buffer to modify
|
|
3247
|
+
* @param changes Cell changes from composeSelectionCells
|
|
3248
|
+
*/
|
|
3249
|
+
function applySelectionToBuffer(buffer, changes) {
|
|
3250
|
+
for (const change of changes) {
|
|
3251
|
+
const cell = buffer.getCell(change.col, change.row);
|
|
3252
|
+
if (change.inverseAttr) buffer.setCell(change.col, change.row, {
|
|
3253
|
+
...cell,
|
|
3254
|
+
attrs: {
|
|
3255
|
+
...cell.attrs,
|
|
3256
|
+
inverse: !cell.attrs.inverse
|
|
3257
|
+
}
|
|
3258
|
+
});
|
|
3259
|
+
else buffer.setCell(change.col, change.row, {
|
|
3260
|
+
...cell,
|
|
3261
|
+
fg: normalizeSelectionColor(change.fg) ?? null,
|
|
3262
|
+
bg: normalizeSelectionColor(change.bg) ?? null
|
|
3263
|
+
});
|
|
3264
|
+
}
|
|
3265
|
+
}
|
|
3266
|
+
/**
|
|
3267
|
+
* Compute search-highlight style changes for a list of highlight ranges.
|
|
3268
|
+
*
|
|
3269
|
+
* Returns a sparse list of cell changes (fg/bg only) — apply them with
|
|
3270
|
+
* `applySelectionToBuffer` (the apply step is style-source-agnostic — it
|
|
3271
|
+
* just sets fg/bg or toggles inverse on the cells you give it).
|
|
3272
|
+
*
|
|
3273
|
+
* Default behaviour mirrors the legacy `renderSearchHighlights` that wrote
|
|
3274
|
+
* `\x1b[7m...\x1b[27m` past the buffer: cells with default fg/bg get the
|
|
3275
|
+
* inverse-attr toggle (so the terminal handles the SGR 7 swap at display).
|
|
3276
|
+
* Cells with explicit fg/bg get a direct fg↔bg swap. A theme can override
|
|
3277
|
+
* with explicit `selectionFg` / `selectionBg` tokens.
|
|
3278
|
+
*
|
|
3279
|
+
* Tracking: km-silvery.delete-search-overlay-ansi
|
|
3280
|
+
*/
|
|
3281
|
+
function composeSearchHighlightCells(buffer, highlights, theme) {
|
|
3282
|
+
if (highlights.length === 0) return [];
|
|
3283
|
+
const changes = [];
|
|
3284
|
+
for (const h of highlights) {
|
|
3285
|
+
const row = h.screenRow;
|
|
3286
|
+
if (row < 0 || row >= buffer.height) continue;
|
|
3287
|
+
const colStart = Math.max(0, h.startCol);
|
|
3288
|
+
const colEnd = Math.min(buffer.width - 1, h.endCol);
|
|
3289
|
+
if (colStart > colEnd) continue;
|
|
3290
|
+
for (let col = colStart; col <= colEnd; col++) {
|
|
3291
|
+
if (buffer.isCellContinuation(col, row)) continue;
|
|
3292
|
+
const cellFg = buffer.getCellFg(col, row);
|
|
3293
|
+
const cellBg = buffer.getCellBg(col, row);
|
|
3294
|
+
let newFg;
|
|
3295
|
+
let newBg;
|
|
3296
|
+
let inverseAttr = false;
|
|
3297
|
+
if (theme?.selectionBg != null) {
|
|
3298
|
+
newFg = theme.selectionFg ?? cellFg;
|
|
3299
|
+
newBg = theme.selectionBg;
|
|
3300
|
+
} else if (cellFg == null || cellBg == null) {
|
|
3301
|
+
newFg = cellFg;
|
|
3302
|
+
newBg = cellBg;
|
|
3303
|
+
inverseAttr = true;
|
|
3304
|
+
} else {
|
|
3305
|
+
newFg = cellBg;
|
|
3306
|
+
newBg = cellFg;
|
|
3307
|
+
}
|
|
3308
|
+
changes.push({
|
|
3309
|
+
col,
|
|
3310
|
+
row,
|
|
3311
|
+
fg: newFg,
|
|
3312
|
+
bg: newBg,
|
|
3313
|
+
inverseAttr
|
|
3314
|
+
});
|
|
3315
|
+
}
|
|
3316
|
+
}
|
|
3317
|
+
return changes;
|
|
3318
|
+
}
|
|
3319
|
+
//#endregion
|
|
3320
|
+
//#region packages/ag-term/src/virtual-scrollback.ts
|
|
3321
|
+
/**
|
|
3322
|
+
* Virtual scrollback buffer for storing historical rendered content.
|
|
3323
|
+
*
|
|
3324
|
+
* Used by virtual inline mode to maintain scrollable history while
|
|
3325
|
+
* rendering in altscreen (which normally has no scrollback).
|
|
3326
|
+
*
|
|
3327
|
+
* Implementation uses a circular buffer for O(1) push and bounded memory.
|
|
3328
|
+
*/
|
|
3329
|
+
function createVirtualScrollback(options) {
|
|
3330
|
+
const maxLines = options?.maxLines ?? 1e4;
|
|
3331
|
+
const ansiLines = new Array(maxLines);
|
|
3332
|
+
const plainLines = new Array(maxLines);
|
|
3333
|
+
let head = 0;
|
|
3334
|
+
let count = 0;
|
|
3335
|
+
return {
|
|
3336
|
+
push(lines) {
|
|
3337
|
+
for (const line of lines) {
|
|
3338
|
+
ansiLines[head] = line;
|
|
3339
|
+
plainLines[head] = stripAnsi(line);
|
|
3340
|
+
head = (head + 1) % maxLines;
|
|
3341
|
+
if (count < maxLines) count++;
|
|
3342
|
+
}
|
|
3343
|
+
},
|
|
3344
|
+
getVisibleRows(offset, rowCount) {
|
|
3345
|
+
const result = [];
|
|
3346
|
+
for (let i = 0; i < rowCount; i++) {
|
|
3347
|
+
const logicalIndex = count - offset - rowCount + i;
|
|
3348
|
+
if (logicalIndex < 0 || logicalIndex >= count) result.push("");
|
|
3349
|
+
else {
|
|
3350
|
+
const physical = (head - count + logicalIndex + maxLines) % maxLines;
|
|
3351
|
+
result.push(ansiLines[physical]);
|
|
3352
|
+
}
|
|
3353
|
+
}
|
|
3354
|
+
return result;
|
|
3355
|
+
},
|
|
3356
|
+
get totalLines() {
|
|
3357
|
+
return count;
|
|
3358
|
+
},
|
|
3359
|
+
search(query) {
|
|
3360
|
+
if (!query) return [];
|
|
3361
|
+
const lowerQuery = query.toLowerCase();
|
|
3362
|
+
const matches = [];
|
|
3363
|
+
for (let i = 0; i < count; i++) if (plainLines[(head - count + i + maxLines) % maxLines].toLowerCase().includes(lowerQuery)) matches.push(i);
|
|
3364
|
+
return matches;
|
|
3365
|
+
},
|
|
3366
|
+
clear() {
|
|
3367
|
+
head = 0;
|
|
3368
|
+
count = 0;
|
|
3369
|
+
}
|
|
3370
|
+
};
|
|
3371
|
+
}
|
|
3372
|
+
//#endregion
|
|
3373
|
+
//#region packages/ag-term/src/history-buffer.ts
|
|
3374
|
+
/**
|
|
3375
|
+
* Ring buffer for frozen scrollback items.
|
|
3376
|
+
*
|
|
3377
|
+
* Each item represents a rendered list entry (card, row, etc.) with its
|
|
3378
|
+
* ANSI snapshot, split rows, and plain-text rows for searching.
|
|
3379
|
+
* When total rows or cached ANSI bytes exceed capacity, the oldest items are
|
|
3380
|
+
* evicted. The byte count tracks the serialized string payload; it is a
|
|
3381
|
+
* deterministic cache budget, not an exact process heap measurement.
|
|
3382
|
+
*/
|
|
3383
|
+
function createHistoryItem(key, ansi, width) {
|
|
3384
|
+
const rows = ansi.split("\n");
|
|
3385
|
+
return {
|
|
3386
|
+
key,
|
|
3387
|
+
ansi,
|
|
3388
|
+
rows,
|
|
3389
|
+
plainTextRows: rows.map((r) => stripAnsi(r)),
|
|
3390
|
+
width
|
|
3391
|
+
};
|
|
3392
|
+
}
|
|
3393
|
+
function createHistoryBuffer(options = 1e4) {
|
|
3394
|
+
const capacity = typeof options === "number" ? options : options.capacityRows ?? 1e4;
|
|
3395
|
+
const capacityBytes = typeof options === "number" ? null : normalizeCapacityBytes(options.capacityBytes);
|
|
3396
|
+
let items = [];
|
|
3397
|
+
let _totalRows = 0;
|
|
3398
|
+
let _totalBytes = 0;
|
|
3399
|
+
function evict() {
|
|
3400
|
+
while (items.length > 0 && (_totalRows > capacity || capacityBytes !== null && _totalBytes > capacityBytes)) {
|
|
3401
|
+
const removed = items.shift();
|
|
3402
|
+
_totalRows -= removed.rows.length;
|
|
3403
|
+
_totalBytes -= estimateHistoryItemBytes(removed);
|
|
3404
|
+
}
|
|
3405
|
+
}
|
|
3406
|
+
/** Walk items to find which item contains the given document row. */
|
|
3407
|
+
function resolveRow(row) {
|
|
3408
|
+
if (row < 0 || row >= _totalRows) return null;
|
|
3409
|
+
let cumulative = 0;
|
|
3410
|
+
for (let i = 0; i < items.length; i++) {
|
|
3411
|
+
const itemRows = items[i].rows.length;
|
|
3412
|
+
if (row < cumulative + itemRows) return {
|
|
3413
|
+
itemIndex: i,
|
|
3414
|
+
localRow: row - cumulative
|
|
3415
|
+
};
|
|
3416
|
+
cumulative += itemRows;
|
|
3417
|
+
}
|
|
3418
|
+
return null;
|
|
3419
|
+
}
|
|
3420
|
+
return {
|
|
3421
|
+
push(item) {
|
|
3422
|
+
items.push(item);
|
|
3423
|
+
_totalRows += item.rows.length;
|
|
3424
|
+
_totalBytes += estimateHistoryItemBytes(item);
|
|
3425
|
+
evict();
|
|
3426
|
+
},
|
|
3427
|
+
get totalRows() {
|
|
3428
|
+
return _totalRows;
|
|
3429
|
+
},
|
|
3430
|
+
get totalBytes() {
|
|
3431
|
+
return _totalBytes;
|
|
3432
|
+
},
|
|
3433
|
+
get itemCount() {
|
|
3434
|
+
return items.length;
|
|
3435
|
+
},
|
|
3436
|
+
get capacity() {
|
|
3437
|
+
return capacity;
|
|
3438
|
+
},
|
|
3439
|
+
get capacityBytes() {
|
|
3440
|
+
return capacityBytes;
|
|
3441
|
+
},
|
|
3442
|
+
getRows(startRow, count) {
|
|
3443
|
+
const result = [];
|
|
3444
|
+
for (let r = startRow; r < startRow + count; r++) {
|
|
3445
|
+
const resolved = resolveRow(r);
|
|
3446
|
+
if (resolved) result.push(items[resolved.itemIndex].rows[resolved.localRow]);
|
|
3447
|
+
else result.push("");
|
|
3448
|
+
}
|
|
3449
|
+
return result;
|
|
3450
|
+
},
|
|
3451
|
+
getPlainTextRows(startRow, count) {
|
|
3452
|
+
const result = [];
|
|
3453
|
+
for (let r = startRow; r < startRow + count; r++) {
|
|
3454
|
+
const resolved = resolveRow(r);
|
|
3455
|
+
if (resolved) result.push(items[resolved.itemIndex].plainTextRows[resolved.localRow]);
|
|
3456
|
+
else result.push("");
|
|
3457
|
+
}
|
|
3458
|
+
return result;
|
|
3459
|
+
},
|
|
3460
|
+
search(query) {
|
|
3461
|
+
if (!query) return [];
|
|
3462
|
+
const lowerQuery = query.toLowerCase();
|
|
3463
|
+
const matches = [];
|
|
3464
|
+
let rowOffset = 0;
|
|
3465
|
+
for (const item of items) {
|
|
3466
|
+
for (let r = 0; r < item.plainTextRows.length; r++) if (item.plainTextRows[r].toLowerCase().includes(lowerQuery)) matches.push(rowOffset + r);
|
|
3467
|
+
rowOffset += item.rows.length;
|
|
3468
|
+
}
|
|
3469
|
+
return matches;
|
|
3470
|
+
},
|
|
3471
|
+
getItemAtRow(row) {
|
|
3472
|
+
const resolved = resolveRow(row);
|
|
3473
|
+
if (!resolved) return null;
|
|
3474
|
+
return {
|
|
3475
|
+
item: items[resolved.itemIndex],
|
|
3476
|
+
localRow: resolved.localRow
|
|
3477
|
+
};
|
|
3478
|
+
},
|
|
3479
|
+
clear() {
|
|
3480
|
+
items = [];
|
|
3481
|
+
_totalRows = 0;
|
|
3482
|
+
_totalBytes = 0;
|
|
3483
|
+
}
|
|
3484
|
+
};
|
|
3485
|
+
}
|
|
3486
|
+
function normalizeCapacityBytes(value) {
|
|
3487
|
+
return Number.isFinite(value) && value !== void 0 && value > 0 ? Math.floor(value) : null;
|
|
3488
|
+
}
|
|
3489
|
+
const utf8Encoder = new TextEncoder();
|
|
3490
|
+
function estimateHistoryItemBytes(item) {
|
|
3491
|
+
return utf8Encoder.encode(item.ansi).byteLength;
|
|
3492
|
+
}
|
|
3493
|
+
//#endregion
|
|
3494
|
+
//#region packages/ag-term/src/viewport-compositor.ts
|
|
3495
|
+
function composeViewport(config) {
|
|
3496
|
+
const { history, viewportHeight, scrollOffset } = config;
|
|
3497
|
+
const totalHistory = history.totalRows;
|
|
3498
|
+
if (scrollOffset <= 0 || totalHistory === 0) return {
|
|
3499
|
+
overlayRows: [],
|
|
3500
|
+
overlayRowCount: 0,
|
|
3501
|
+
liveRowsVisible: viewportHeight,
|
|
3502
|
+
isScrolledUp: false,
|
|
3503
|
+
totalHeight: totalHistory + viewportHeight
|
|
3504
|
+
};
|
|
3505
|
+
const startRow = Math.max(0, totalHistory - Math.min(scrollOffset, totalHistory));
|
|
3506
|
+
const rowsToShow = Math.min(viewportHeight, totalHistory - startRow);
|
|
3507
|
+
return {
|
|
3508
|
+
overlayRows: history.getRows(startRow, rowsToShow),
|
|
3509
|
+
overlayRowCount: rowsToShow,
|
|
3510
|
+
liveRowsVisible: viewportHeight - rowsToShow,
|
|
3511
|
+
isScrolledUp: true,
|
|
3512
|
+
totalHeight: totalHistory + viewportHeight
|
|
3513
|
+
};
|
|
3514
|
+
}
|
|
3515
|
+
//#endregion
|
|
3516
|
+
//#region packages/ag-term/src/list-document.ts
|
|
3517
|
+
function createListDocument(history, getLiveItems) {
|
|
3518
|
+
function liveRowCount() {
|
|
3519
|
+
let total = 0;
|
|
3520
|
+
for (const block of getLiveItems()) total += block.rows.length;
|
|
3521
|
+
return total;
|
|
3522
|
+
}
|
|
3523
|
+
return {
|
|
3524
|
+
get totalRows() {
|
|
3525
|
+
return history.totalRows + liveRowCount();
|
|
3526
|
+
},
|
|
3527
|
+
get frozenRows() {
|
|
3528
|
+
return history.totalRows;
|
|
3529
|
+
},
|
|
3530
|
+
get liveRows() {
|
|
3531
|
+
return liveRowCount();
|
|
3532
|
+
},
|
|
3533
|
+
getRows(startRow, count) {
|
|
3534
|
+
const frozen = history.totalRows;
|
|
3535
|
+
const result = [];
|
|
3536
|
+
for (let r = startRow; r < startRow + count; r++) if (r < 0 || r >= this.totalRows) result.push("");
|
|
3537
|
+
else if (r < frozen) result.push(...history.getRows(r, 1));
|
|
3538
|
+
else {
|
|
3539
|
+
let liveRow = r - frozen;
|
|
3540
|
+
let found = false;
|
|
3541
|
+
for (const block of getLiveItems()) {
|
|
3542
|
+
if (liveRow < block.rows.length) {
|
|
3543
|
+
result.push(block.rows[liveRow]);
|
|
3544
|
+
found = true;
|
|
3545
|
+
break;
|
|
3546
|
+
}
|
|
3547
|
+
liveRow -= block.rows.length;
|
|
3548
|
+
}
|
|
3549
|
+
if (!found) result.push("");
|
|
3550
|
+
}
|
|
3551
|
+
return result;
|
|
3552
|
+
},
|
|
3553
|
+
getPlainTextRows(startRow, count) {
|
|
3554
|
+
const frozen = history.totalRows;
|
|
3555
|
+
const result = [];
|
|
3556
|
+
for (let r = startRow; r < startRow + count; r++) if (r < 0 || r >= this.totalRows) result.push("");
|
|
3557
|
+
else if (r < frozen) result.push(...history.getPlainTextRows(r, 1));
|
|
3558
|
+
else {
|
|
3559
|
+
let liveRow = r - frozen;
|
|
3560
|
+
let found = false;
|
|
3561
|
+
for (const block of getLiveItems()) {
|
|
3562
|
+
if (liveRow < block.plainTextRows.length) {
|
|
3563
|
+
result.push(block.plainTextRows[liveRow]);
|
|
3564
|
+
found = true;
|
|
3565
|
+
break;
|
|
3566
|
+
}
|
|
3567
|
+
liveRow -= block.plainTextRows.length;
|
|
3568
|
+
}
|
|
3569
|
+
if (!found) result.push("");
|
|
3570
|
+
}
|
|
3571
|
+
return result;
|
|
3572
|
+
},
|
|
3573
|
+
getSource(row) {
|
|
3574
|
+
const frozen = history.totalRows;
|
|
3575
|
+
if (row < 0 || row >= this.totalRows) return null;
|
|
3576
|
+
if (row < frozen) {
|
|
3577
|
+
const hit = history.getItemAtRow(row);
|
|
3578
|
+
if (!hit) return null;
|
|
3579
|
+
return {
|
|
3580
|
+
type: "frozen",
|
|
3581
|
+
itemKey: hit.item.key,
|
|
3582
|
+
localRow: hit.localRow
|
|
3583
|
+
};
|
|
3584
|
+
}
|
|
3585
|
+
const liveItems = getLiveItems();
|
|
3586
|
+
let liveRow = row - frozen;
|
|
3587
|
+
for (const block of liveItems) {
|
|
3588
|
+
if (liveRow < block.rows.length) return {
|
|
3589
|
+
type: "live",
|
|
3590
|
+
itemIndex: block.itemIndex,
|
|
3591
|
+
localRow: liveRow
|
|
3592
|
+
};
|
|
3593
|
+
liveRow -= block.rows.length;
|
|
3594
|
+
}
|
|
3595
|
+
return null;
|
|
3596
|
+
},
|
|
3597
|
+
search(query) {
|
|
3598
|
+
if (!query) return [];
|
|
3599
|
+
const lowerQuery = query.toLowerCase();
|
|
3600
|
+
const matches = [];
|
|
3601
|
+
const frozen = history.totalRows;
|
|
3602
|
+
const frozenRowMatches = history.search(query);
|
|
3603
|
+
for (const row of frozenRowMatches) {
|
|
3604
|
+
const line = history.getPlainTextRows(row, 1)[0].toLowerCase();
|
|
3605
|
+
let pos = line.indexOf(lowerQuery);
|
|
3606
|
+
while (pos !== -1) {
|
|
3607
|
+
matches.push({
|
|
3608
|
+
row,
|
|
3609
|
+
startCol: pos,
|
|
3610
|
+
endCol: pos + query.length
|
|
3611
|
+
});
|
|
3612
|
+
pos = line.indexOf(lowerQuery, pos + 1);
|
|
3613
|
+
}
|
|
3614
|
+
}
|
|
3615
|
+
let rowOffset = 0;
|
|
3616
|
+
for (const block of getLiveItems()) {
|
|
3617
|
+
for (let i = 0; i < block.plainTextRows.length; i++) {
|
|
3618
|
+
const line = block.plainTextRows[i].toLowerCase();
|
|
3619
|
+
let pos = line.indexOf(lowerQuery);
|
|
3620
|
+
while (pos !== -1) {
|
|
3621
|
+
matches.push({
|
|
3622
|
+
row: frozen + rowOffset + i,
|
|
3623
|
+
startCol: pos,
|
|
3624
|
+
endCol: pos + query.length
|
|
3625
|
+
});
|
|
3626
|
+
pos = line.indexOf(lowerQuery, pos + 1);
|
|
3627
|
+
}
|
|
3628
|
+
}
|
|
3629
|
+
rowOffset += block.plainTextRows.length;
|
|
3630
|
+
}
|
|
3631
|
+
return matches;
|
|
3632
|
+
}
|
|
3633
|
+
};
|
|
3634
|
+
}
|
|
3635
|
+
//#endregion
|
|
3636
|
+
//#region packages/ag-term/src/text-surface.ts
|
|
3637
|
+
function createTextSurface(config) {
|
|
3638
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
3639
|
+
function notify() {
|
|
3640
|
+
for (const fn of listeners) fn();
|
|
3641
|
+
}
|
|
3642
|
+
return {
|
|
3643
|
+
get id() {
|
|
3644
|
+
return config.id;
|
|
3645
|
+
},
|
|
3646
|
+
get document() {
|
|
3647
|
+
return config.document;
|
|
3648
|
+
},
|
|
3649
|
+
getText(startRow, startCol, endRow, endCol) {
|
|
3650
|
+
const rows = config.document.getRows(startRow, endRow - startRow + 1);
|
|
3651
|
+
const lines = [];
|
|
3652
|
+
for (let i = 0; i < rows.length; i++) {
|
|
3653
|
+
const plain = stripAnsi(rows[i]);
|
|
3654
|
+
const row = startRow + i;
|
|
3655
|
+
if (row === startRow && row === endRow) lines.push(plain.slice(startCol, endCol));
|
|
3656
|
+
else if (row === startRow) lines.push(plain.slice(startCol));
|
|
3657
|
+
else if (row === endRow) lines.push(plain.slice(0, endCol));
|
|
3658
|
+
else lines.push(plain);
|
|
3659
|
+
}
|
|
3660
|
+
return lines.join("\n");
|
|
3661
|
+
},
|
|
3662
|
+
search(query) {
|
|
3663
|
+
return config.document.search(query);
|
|
3664
|
+
},
|
|
3665
|
+
hitTest(viewportRow, viewportCol) {
|
|
3666
|
+
const docRow = config.viewportToDocument(viewportRow);
|
|
3667
|
+
if (docRow < 0 || docRow >= config.document.totalRows) return null;
|
|
3668
|
+
return {
|
|
3669
|
+
row: docRow,
|
|
3670
|
+
col: viewportCol
|
|
3671
|
+
};
|
|
3672
|
+
},
|
|
3673
|
+
reveal(row) {
|
|
3674
|
+
config.onReveal(row);
|
|
3675
|
+
notify();
|
|
3676
|
+
},
|
|
3677
|
+
notifyContentChange() {
|
|
3678
|
+
notify();
|
|
3679
|
+
},
|
|
3680
|
+
subscribe(listener) {
|
|
3681
|
+
listeners.add(listener);
|
|
3682
|
+
return () => {
|
|
3683
|
+
listeners.delete(listener);
|
|
3684
|
+
};
|
|
3685
|
+
},
|
|
3686
|
+
get capabilities() {
|
|
3687
|
+
return config.capabilities;
|
|
3688
|
+
}
|
|
3689
|
+
};
|
|
3690
|
+
}
|
|
3691
|
+
//#endregion
|
|
3692
|
+
//#region packages/ag-term/src/search-overlay.ts
|
|
3693
|
+
/**
|
|
3694
|
+
* Find all case-insensitive occurrences of `query` inside `text` and return
|
|
3695
|
+
* their character offsets. Empty query or empty text returns `[]`.
|
|
3696
|
+
*
|
|
3697
|
+
* This is the canonical silvery search-match algorithm. The same logic runs
|
|
3698
|
+
* inside `ListView`'s registered Searchable (to find matches across items)
|
|
3699
|
+
* and is exposed to consumers that render multi-segment items — a LogRow
|
|
3700
|
+
* whose searchable text is a concatenation of field values, but whose visual
|
|
3701
|
+
* rendering splits those fields across separate Text nodes, needs per-segment
|
|
3702
|
+
* ranges to highlight without re-implementing the semantics.
|
|
3703
|
+
*
|
|
3704
|
+
* Ranges are returned in ascending `start` order; overlapping matches are
|
|
3705
|
+
* not produced (`indexOf(..., start = last.start + 1)` advances past each
|
|
3706
|
+
* match start, not past its full length, preserving overlapping runs of
|
|
3707
|
+
* short queries — e.g. `"aa"` in `"aaaa"` yields `[0..2], [1..3], [2..4]`).
|
|
3708
|
+
*
|
|
3709
|
+
* Offsets are CHARACTER offsets into the input string. They are not column
|
|
3710
|
+
* offsets — multi-column wide glyphs are counted as one character here, the
|
|
3711
|
+
* same way the SearchProvider's match-row computation does.
|
|
3712
|
+
*/
|
|
3713
|
+
function computeMatchRanges(text, query) {
|
|
3714
|
+
if (query === "" || text === "") return [];
|
|
3715
|
+
const lowerText = text.toLowerCase();
|
|
3716
|
+
const lowerQuery = query.toLowerCase();
|
|
3717
|
+
const ranges = [];
|
|
3718
|
+
let cursor = 0;
|
|
3719
|
+
while (cursor < lowerText.length) {
|
|
3720
|
+
const found = lowerText.indexOf(lowerQuery, cursor);
|
|
3721
|
+
if (found === -1) break;
|
|
3722
|
+
ranges.push({
|
|
3723
|
+
start: found,
|
|
3724
|
+
end: found + query.length
|
|
3725
|
+
});
|
|
3726
|
+
cursor = found + 1;
|
|
3727
|
+
}
|
|
3728
|
+
return ranges;
|
|
3729
|
+
}
|
|
3730
|
+
function createSearchState() {
|
|
3731
|
+
return {
|
|
3732
|
+
active: false,
|
|
3733
|
+
query: "",
|
|
3734
|
+
matches: [],
|
|
3735
|
+
currentMatch: -1,
|
|
3736
|
+
cursorPosition: 0
|
|
3737
|
+
};
|
|
3738
|
+
}
|
|
3739
|
+
/**
|
|
3740
|
+
* Update search state. searchFn is called when query changes to find matches.
|
|
3741
|
+
*/
|
|
3742
|
+
function searchUpdate(action, state, searchFn) {
|
|
3743
|
+
switch (action.type) {
|
|
3744
|
+
case "open": return [{
|
|
3745
|
+
...state,
|
|
3746
|
+
active: true,
|
|
3747
|
+
query: "",
|
|
3748
|
+
matches: [],
|
|
3749
|
+
currentMatch: -1,
|
|
3750
|
+
cursorPosition: 0
|
|
3751
|
+
}, [{ type: "render" }]];
|
|
3752
|
+
case "close": return [{
|
|
3753
|
+
...state,
|
|
3754
|
+
active: false,
|
|
3755
|
+
query: "",
|
|
3756
|
+
cursorPosition: 0
|
|
3757
|
+
}, [{ type: "render" }]];
|
|
3758
|
+
case "input": {
|
|
3759
|
+
const query = state.query.slice(0, state.cursorPosition) + action.char + state.query.slice(state.cursorPosition);
|
|
3760
|
+
const cursorPosition = state.cursorPosition + 1;
|
|
3761
|
+
const matches = searchFn ? searchFn(query) : [];
|
|
3762
|
+
const currentMatch = matches.length > 0 ? 0 : -1;
|
|
3763
|
+
const effects = [{ type: "render" }];
|
|
3764
|
+
if (currentMatch >= 0) effects.push({
|
|
3765
|
+
type: "scrollTo",
|
|
3766
|
+
row: matches[0].row
|
|
3767
|
+
});
|
|
3768
|
+
return [{
|
|
3769
|
+
...state,
|
|
3770
|
+
query,
|
|
3771
|
+
cursorPosition,
|
|
3772
|
+
matches,
|
|
3773
|
+
currentMatch
|
|
3774
|
+
}, effects];
|
|
3775
|
+
}
|
|
3776
|
+
case "backspace": {
|
|
3777
|
+
if (state.cursorPosition === 0) return [state, []];
|
|
3778
|
+
const query = state.query.slice(0, state.cursorPosition - 1) + state.query.slice(state.cursorPosition);
|
|
3779
|
+
const cursorPosition = state.cursorPosition - 1;
|
|
3780
|
+
const matches = searchFn ? searchFn(query) : [];
|
|
3781
|
+
const currentMatch = matches.length > 0 ? 0 : -1;
|
|
3782
|
+
const effects = [{ type: "render" }];
|
|
3783
|
+
if (currentMatch >= 0) effects.push({
|
|
3784
|
+
type: "scrollTo",
|
|
3785
|
+
row: matches[0].row
|
|
3786
|
+
});
|
|
3787
|
+
return [{
|
|
3788
|
+
...state,
|
|
3789
|
+
query,
|
|
3790
|
+
cursorPosition,
|
|
3791
|
+
matches,
|
|
3792
|
+
currentMatch
|
|
3793
|
+
}, effects];
|
|
3794
|
+
}
|
|
3795
|
+
case "nextMatch": {
|
|
3796
|
+
if (state.matches.length === 0) return [state, []];
|
|
3797
|
+
const currentMatch = (state.currentMatch + 1) % state.matches.length;
|
|
3798
|
+
return [{
|
|
3799
|
+
...state,
|
|
3800
|
+
currentMatch
|
|
3801
|
+
}, [{
|
|
3802
|
+
type: "scrollTo",
|
|
3803
|
+
row: state.matches[currentMatch].row
|
|
3804
|
+
}, { type: "render" }]];
|
|
3805
|
+
}
|
|
3806
|
+
case "prevMatch": {
|
|
3807
|
+
if (state.matches.length === 0) return [state, []];
|
|
3808
|
+
const currentMatch = (state.currentMatch - 1 + state.matches.length) % state.matches.length;
|
|
3809
|
+
return [{
|
|
3810
|
+
...state,
|
|
3811
|
+
currentMatch
|
|
3812
|
+
}, [{
|
|
3813
|
+
type: "scrollTo",
|
|
3814
|
+
row: state.matches[currentMatch].row
|
|
3815
|
+
}, { type: "render" }]];
|
|
3816
|
+
}
|
|
3817
|
+
case "cursorLeft":
|
|
3818
|
+
if (state.cursorPosition === 0) return [state, []];
|
|
3819
|
+
return [{
|
|
3820
|
+
...state,
|
|
3821
|
+
cursorPosition: state.cursorPosition - 1
|
|
3822
|
+
}, []];
|
|
3823
|
+
case "cursorRight":
|
|
3824
|
+
if (state.cursorPosition >= state.query.length) return [state, []];
|
|
3825
|
+
return [{
|
|
3826
|
+
...state,
|
|
3827
|
+
cursorPosition: state.cursorPosition + 1
|
|
3828
|
+
}, []];
|
|
3829
|
+
case "cursorToStart":
|
|
3830
|
+
if (state.cursorPosition === 0) return [state, []];
|
|
3831
|
+
return [{
|
|
3832
|
+
...state,
|
|
3833
|
+
cursorPosition: 0
|
|
3834
|
+
}, []];
|
|
3835
|
+
case "cursorToEnd":
|
|
3836
|
+
if (state.cursorPosition >= state.query.length) return [state, []];
|
|
3837
|
+
return [{
|
|
3838
|
+
...state,
|
|
3839
|
+
cursorPosition: state.query.length
|
|
3840
|
+
}, []];
|
|
3841
|
+
case "deleteWordBack": {
|
|
3842
|
+
if (state.cursorPosition === 0) return [state, []];
|
|
3843
|
+
let pos = state.cursorPosition;
|
|
3844
|
+
while (pos > 0 && /\s/.test(state.query[pos - 1] ?? "")) pos--;
|
|
3845
|
+
while (pos > 0 && !/\s/.test(state.query[pos - 1] ?? "")) pos--;
|
|
3846
|
+
const query = state.query.slice(0, pos) + state.query.slice(state.cursorPosition);
|
|
3847
|
+
const cursorPosition = pos;
|
|
3848
|
+
const matches = searchFn ? searchFn(query) : [];
|
|
3849
|
+
const currentMatch = matches.length > 0 ? 0 : -1;
|
|
3850
|
+
const effects = [{ type: "render" }];
|
|
3851
|
+
if (currentMatch >= 0) effects.push({
|
|
3852
|
+
type: "scrollTo",
|
|
3853
|
+
row: matches[0].row
|
|
3854
|
+
});
|
|
3855
|
+
return [{
|
|
3856
|
+
...state,
|
|
3857
|
+
query,
|
|
3858
|
+
cursorPosition,
|
|
3859
|
+
matches,
|
|
3860
|
+
currentMatch
|
|
3861
|
+
}, effects];
|
|
3862
|
+
}
|
|
3863
|
+
case "deleteToStart": {
|
|
3864
|
+
if (state.cursorPosition === 0) return [state, []];
|
|
3865
|
+
const query = state.query.slice(state.cursorPosition);
|
|
3866
|
+
const cursorPosition = 0;
|
|
3867
|
+
const matches = searchFn ? searchFn(query) : [];
|
|
3868
|
+
const currentMatch = matches.length > 0 ? 0 : -1;
|
|
3869
|
+
const effects = [{ type: "render" }];
|
|
3870
|
+
if (currentMatch >= 0) effects.push({
|
|
3871
|
+
type: "scrollTo",
|
|
3872
|
+
row: matches[0].row
|
|
3873
|
+
});
|
|
3874
|
+
return [{
|
|
3875
|
+
...state,
|
|
3876
|
+
query,
|
|
3877
|
+
cursorPosition,
|
|
3878
|
+
matches,
|
|
3879
|
+
currentMatch
|
|
3880
|
+
}, effects];
|
|
3881
|
+
}
|
|
3882
|
+
}
|
|
3883
|
+
}
|
|
3884
|
+
/**
|
|
3885
|
+
* Render the search bar as a plain (no ANSI) string padded to `cols` width.
|
|
3886
|
+
* Format: " / query [2/15]" or " / query [no matches]" — padded with spaces.
|
|
3887
|
+
*
|
|
3888
|
+
* Caller decides how to display it — wrap with `\x1b[7m...\x1b[27m` for the
|
|
3889
|
+
* legacy overlay path, or stamp into a buffer's cells with the inverse
|
|
3890
|
+
* attribute for the compose+apply path
|
|
3891
|
+
* (see `applySearchBarToPaintBuffer` in `runtime/renderer.ts`).
|
|
3892
|
+
*/
|
|
3893
|
+
function renderSearchBarPlain(state, cols) {
|
|
3894
|
+
const prefix = " / ";
|
|
3895
|
+
const matchInfo = state.matches.length > 0 ? ` [${state.currentMatch + 1}/${state.matches.length}]` : state.query ? " [no matches]" : "";
|
|
3896
|
+
return (prefix + state.query + matchInfo).padEnd(cols);
|
|
3897
|
+
}
|
|
3898
|
+
//#endregion
|
|
3899
|
+
export { DecMode as $, withMeasurer as A, createTerminalSelectionState as At, resolveNonTTYMode as B, resetScrollRegion as C, CursorProvider as Ct, supportsScrollRegions as D, createDragEvent as Dt, setScrollRegion as E, useCursor as Et, inspectTree as F, createInputEvents as G, Z_INDEX as H, isInspectorEnabled as I, resolveFromTerm as J, isTerm as K, countLines as L, disableInspector as M, terminalSelectionUpdate as Mt, enableInspector as N, createFocusManager as Nt, calcEdgeBasedScrollOffset as O, createDragState as Ot, inspectFrame as P, queryTextAreaSize as Q, createOutputTransformer as R, moveCursor as S, createCanvasAdapter as St, scrollUp as T, resetCursorState as Tt, generateHitRegionId as U, HitRegistry as V, resetHitRegionIdCounter as W, queryCellSize as X, resolveTermDef as Y, queryTextAreaPixels as Z, getTabOrder as _, findActiveCursorNode as _t, createTextSurface as a, queryTerminalVersion as at, splitPane as b, injectDOMStyles as bt, createHistoryBuffer as c, detectWidthConfigWithProbe as ct, applySelectionToBuffer as d, TERMTEST_SECTIONS as dt, queryMode as et, composeSearchHighlightCells as f, runTermtest as ft, getPaneIds as g, OSC133 as gt, findAdjacentPane as h, detectKittyWithProbe as ht, searchUpdate as i, querySecondaryDA as it, autoEnableInspector as j, extractText as jt, createPipeline as k, findDropTarget as kt, createHistoryItem as l, queryCursorFromStdio as lt, createLeaf as m, detectKittySupport as mt, createSearchState as n, queryDeviceAttributes as nt, createListDocument as o, queryTertiaryDA as ot, composeSelectionCells as p, detectKittyFromStdio as pt, isTermDef as q, renderSearchBarPlain as r, queryPrimaryDA as rt, composeViewport as s, applyWidthConfig as st, computeMatchRanges as t, queryModes as tt, createVirtualScrollback as u, queryCursorPosition as ut, removePane as v, DOMRenderBuffer as vt, scrollDown as w, createCursorStore as wt, swapPanes as x, CanvasRenderBuffer as xt, resizeSplit as y, createDOMAdapter as yt, isTTY as z };
|
|
3900
|
+
|
|
3901
|
+
//# sourceMappingURL=src-DbqcSrhy.mjs.map
|