update at 2026-03-04 13:25:26

This commit is contained in:
douboer@gmail.com
2026-03-04 13:25:26 +08:00
parent 7d2be3d67d
commit 5fbfdc651f
44 changed files with 84 additions and 1 deletions

View File

@@ -0,0 +1,28 @@
const fs = require('fs');
let code = fs.readFileSync('src/components/TerminalPanel.vue', 'utf8');
// The place where we can attach is right after viewportScroller is queried.
// Let's find viewportScroller assignment.
const attachCode = `
viewportScroller = containerRef.value.querySelector(".xterm-viewport");
if (viewportScroller) {
// ---- [VIEWPORT DEBUG START] ----
['touchstart', 'touchmove', 'touchend', 'touchcancel'].forEach(type => {
viewportScroller.addEventListener(type, (event) => {
console.log(\`[Viewport Event] \${type}\`, {
target: (event.target as Node)?.nodeName,
eventPhase: event.eventPhase,
cancelable: event.cancelable,
defaultPrevented: event.defaultPrevented,
touches: (event as TouchEvent).touches?.length
});
}, { capture: true, passive: false });
});
// ---- [VIEWPORT DEBUG END] ----
}
`;
code = code.replace(/viewportScroller = containerRef\.value\.querySelector\("\.xterm-viewport"\);\n\s*if \(\!viewportScroller\) \{/m, attachCode + "\n if (!viewportScroller) {");
fs.writeFileSync('src/components/TerminalPanel.vue', code);

View File

@@ -0,0 +1,13 @@
const fs = require('fs');
let code = fs.readFileSync('src/components/TerminalPanel.vue', 'utf8');
// remove multiline logs
code = code.replace(/console\.log\(\s*\[Scroll Deep\][ \w\W]*?\);/g, '');
// remove redundant empty else blocks
code = code.replace(/\s*\} else \{\s*\}/g, '');
// remove duplicated momentum logic
code = code.replace(/(\s*\/\/ 释放时触发滚行动量\s*if \(Math\.abs\(touchScrollVelocity\) > 0\.2 && touchGateScrollLike && !hasActiveNativeSelectionInTerminal\(\)\) \{\s*touchScrollVelocity \*= 1\.35;\s*runTouchScrollMomentum\(\);\s*\})\s*\/\/ 释放时触发滚行动量\s*if \(Math\.abs\(touchScrollVelocity\) > 0\.2 && touchGateScrollLike && !hasActiveNativeSelectionInTerminal\(\)\) \{\s*touchScrollVelocity \*= 1\.35;\s*runTouchScrollMomentum\(\);\s*\}/g, '$1');
fs.writeFileSync('src/components/TerminalPanel.vue', code);

View File

@@ -0,0 +1,17 @@
const fs = require('fs');
let code = fs.readFileSync('src/components/TerminalPanel.vue', 'utf8');
// 1. Remove global event debug block
code = code.replace(/\/\/ \-\-\- 极致全局监听器,抓出是谁吃掉了 move \-\-\-[\s\S]*?\/\/ 用最顶级的 window capture 拦截,这样在任何人(包括 xterm 内部那些黑盒代码)前面执行\n\s*window\.addEventListener\(type, globalEventDebug, \{ capture: true, passive: false \}\);\n\s*\}\);\n/g, '');
// 2. Remove all console.log("[Scroll Deep]...")
code = code.replace(/\s*console\.log\(\s*`?\[Scroll Deep\].*?\);\n/g, '\n');
// 3. Remove useless commented out stopImmediatePropagation and preventDefault blocks left from debugging
const commentedOutRegex1 = /\s*\/\/ event\.stopImmediatePropagation\(\);/g;
code = code.replace(commentedOutRegex1, '');
const commentedOutRegex2 = /\s*\/\/ if \(event\.cancelable\) \{\n\s*\/\/ event\.preventDefault\(\); \/\/ 我们使用 JS 控制滚动,必须禁止系统原生滚动争抢\n\s*\/\/\}/g;
code = code.replace(commentedOutRegex2, '');
fs.writeFileSync('src/components/TerminalPanel.vue', code);

View File

@@ -0,0 +1,5 @@
const fs = require('fs');
let code = fs.readFileSync('src/components/TerminalPanel.vue', 'utf8');
code = code.replace(/let touchScrollLastY = 0;\n/g, '');
code = code.replace(/let touchScrollLastTime = 0;\n/g, '');
fs.writeFileSync('src/components/TerminalPanel.vue', code);

View File

@@ -0,0 +1,8 @@
const fs = require('fs');
let code = fs.readFileSync('src/components/TerminalPanel.vue', 'utf8');
// The issue is an extra brace that was probably around clearTouchScrollMomentum (which I removed with regex `function clearTouchScrollMomentum[\s\S]*?\n\}\n`, but maybe it was `function clearTouchScrollMomentum() { ... } \n }` which left a flying `}`.
code = code.replace(/wheelInertiaVelocity = 0;\n\}\n\}\n/g, 'wheelInertiaVelocity = 0;\n}\n\n');
fs.writeFileSync('src/components/TerminalPanel.vue', code);

View File

@@ -0,0 +1,8 @@
const fs = require('fs');
let code = fs.readFileSync('src/styles/main.css', 'utf8');
code += `
/* Fix for native scrolling: .xterm-screen cannot be scrolled natively because it is not the scroll container.
To allow native scrolling, we would have to make .xterm-screen point-events: none, BUT we need selection on it. */
`;
fs.writeFileSync('src/styles/main.css', code);

View File

@@ -0,0 +1,13 @@
const fs = require('fs');
let code = fs.readFileSync('src/components/TerminalPanel.vue', 'utf8');
// 1. Remove that duplicate touchstart preventDefault logic completely
code = code.replace(/\s*\/\/ 必须阻止 touchstart.*\n\s*if \(event\.cancelable.*\n\s*event\.preventDefault\(\);\s*\n\s*\}/g, '');
// 2. In touchmove, add the preventDefault ONLY IF selection is not active
code = code.replace(/(\/\/ Stop xterm from intercepting this manually\s*\n\s*event\.stopImmediatePropagation\(\);)/g, `$1\n\n // 阻止 iOS 原生滚动,确保 move 事件不被吞噬\n if (event.cancelable) { event.preventDefault(); }\n`);
// 3. Let's remove the duplicated pointer scroll logic since we rely on touchmove for scroll
code = code.replace(/\/\/ --- Pointer Scroll Sync ---.*?\/\/ --- Pointer Scroll Sync End ---/gs, '');
fs.writeFileSync('src/components/TerminalPanel.vue', code);

View File

@@ -0,0 +1,7 @@
const fs = require('fs');
let code = fs.readFileSync('src/components/TerminalPanel.vue', 'utf8');
const regex = /\/\/ \-\-\- Pointer Scroll Sync \-\-\-.*?(?=\s*\}\s*;\s*onTouchKeyboardPointerUp)/s;
code = code.replace(regex, '');
fs.writeFileSync('src/components/TerminalPanel.vue', code);

View File

@@ -0,0 +1,14 @@
const fs = require('fs');
let code = fs.readFileSync('src/components/TerminalPanel.vue', 'utf8');
// I need to ensure that pointermove/pointerdown is NOT swallowing the touch event in mobile safari.
// Let's remove stopImmediatePropagation in the new logic if it's there.
// If touchmove just calls event.stopImmediatePropagation(), then xterm doesn't see it, but DOES it stop the native scroll if xterm is absolute positioned over the viewport?
// Wait, xterm's architecture:
// .xterm-screen is absolute positioned OVER .xterm-viewport.
// If user touches .xterm-screen (zIndex 1?), the touch is on .xterm-screen.
// .xterm-screen DOES NOT HAVE overflow-y: scroll. It's fixed height!
// Therefore, iOS panning gesture on .xterm-screen does NOT bubble to .xterm-viewport as a native scroll!
console.log("Analyzing xterm architecture...");

View File

@@ -0,0 +1,4 @@
const fs = require('fs');
let code = fs.readFileSync('src/components/TerminalPanel.vue', 'utf8');
code = code.replace(/containerRef\.value!\.addEventListener\("touchmove", onTouchKeyboardTouchMove!, \{ capture: true, passive: false \}\);/g, 'containerRef.value!.addEventListener("touchmove", onTouchKeyboardTouchMove!, { capture: true, passive: true });');
fs.writeFileSync('src/components/TerminalPanel.vue', code);

View File

@@ -0,0 +1,12 @@
const fs = require('fs');
let code = fs.readFileSync('src/components/TerminalPanel.vue', 'utf8');
// The reason touchmove stops is because touch events on iOS are sometimes silently swallowed if the pointer event stops propagation but touch doesn't, OR if touch-action hasn't mapped.
// We added touch-action: none. Let's ALSO remove stopImmediatePropagation from pointermove and let standard Pointer Events do their thing!
// Actually, earlier we added:
// onTouchKeyboardPointerMove = (...) { event.stopImmediatePropagation() }
// If we stop pointermove, the browser might think the gesture is dead.
code = code.replace(/event\.stopImmediatePropagation\(\);/g, `// event.stopImmediatePropagation();`);
fs.writeFileSync('src/components/TerminalPanel.vue', code);

View File

@@ -0,0 +1,124 @@
const fs = require('fs');
let code = fs.readFileSync('src/components/TerminalPanel.vue', 'utf8');
// Convert Touch Events to Pointer Events for manual scroll tracking.
// Since touchmove vanishes but pointermove remains active (with target changing from SPAN to DIV, etc)
// Let's modify pointer_move to handle scrolling
let pointerMoveCode = ` onTouchKeyboardPointerMove = (event: PointerEvent) => {
// 保持之前的检查,跳除非 touch 事件等
if (event.pointerType !== "touch" || touchGatePointerId !== event.pointerId) {
return;
}
const dx = event.clientX - touchGateStartX;
const dy = event.clientY - touchGateStartY;
const absDx = Math.abs(dx);
const absDy = Math.abs(dy);
if (absDx > TOUCH_KEYBOARD_TAP_MAX_MOVE_PX || absDy > TOUCH_KEYBOARD_TAP_MAX_MOVE_PX) {
touchGateMoved = true;
}
if (absDy > absDx && absDy > TOUCH_KEYBOARD_TAP_MAX_MOVE_PX) {
touchGateScrollLike = true;
}
// 我们不再拦截 touchmove改用 pointermove 来计算自己的滚动量
const hasSelection = hasActiveNativeSelectionInTerminal();
if (!hasSelection) {
const now = performance.now();
const dt = now - touchScrollLastTime;
const currentY = event.clientY;
const stepDy = currentY - touchScrollLastY;
// 屏蔽初次的巨大跳跃(手指刚按下时)
if (dt > 0 && Math.abs(stepDy) < 200 && touchScrollLastTime > 0) {
if (viewportScroller && stepDy !== 0) {
viewportScroller.scrollTop -= stepDy;
}
const v = (-stepDy / dt) * 16;
if (touchScrollVelocity * v > 0) {
touchScrollVelocity = touchScrollVelocity * 0.5 + v * 0.5;
} else {
touchScrollVelocity = v;
}
const touchMaxSpeed = 120;
if (touchScrollVelocity > touchMaxSpeed) touchScrollVelocity = touchMaxSpeed;
else if (touchScrollVelocity < -touchMaxSpeed) touchScrollVelocity = -touchMaxSpeed;
}
touchScrollLastY = currentY;
touchScrollLastTime = now;
}
// 由于使用了 pointer不要无脑阻止原生行为但我们要阻止 xterm 被选中
// event.stopImmediatePropagation(); // 我们前面通过 touch-action: none 已经避免了系统层面缩放
};`;
let pointerDownCode = ` onTouchKeyboardPointerDown = (event: PointerEvent) => {
console.log("[Scroll Deep] 🟡 POINTER DOWN: ", { pointerId: event.pointerId, type: event.pointerType, target: (event.target as Node)?.nodeName });
if (event.pointerType !== "touch") {
return;
}
if (!helperTextarea) {
return;
}
touchGatePointerId = event.pointerId;
touchGateStartX = event.clientX;
touchGateStartY = event.clientY;
touchGateMoved = false;
touchGateScrollLike = false;
touchGateStartInBand = isTouchInCursorActivationBand(event.clientY);
touchGateHadSelectionAtStart = hasActiveNativeSelectionInTerminal();
// == 初始化滚动参数 ==
clearTouchScrollMomentum();
touchScrollLastY = event.clientY;
touchScrollLastTime = performance.now();
touchScrollVelocity = 0;
if (DEBUG_TOUCH_FOCUS) {
console.log("[TouchFocus] pointerdown", {
inBand: touchGateStartInBand,
selectionStart: touchGateHadSelectionAtStart,
});
}
event.stopImmediatePropagation();
if (event.cancelable) {
event.preventDefault();
}
helperTextarea.readOnly = true;
if (sessionStore.state !== "connected") {
return;
}
};`;
let pointerUpCode = ` onTouchKeyboardPointerUp = (event: PointerEvent) => {
console.log("[Scroll Deep] 🟠 POINTER UP: ", { pointerId: event.pointerId, type: event.pointerType });
if (event.pointerType !== "touch" || touchGatePointerId !== event.pointerId) {
return;
}
// 释放时触发滚行动量
const threshold = 0.2;
if (Math.abs(touchScrollVelocity) > threshold && touchGateScrollLike && !hasActiveNativeSelectionInTerminal()) {
touchScrollVelocity *= 1.35;
console.log(\`[Scroll Deep] 🚀 Pointer 释放触发JS惯性速度=\${touchScrollVelocity.toFixed(2)}\`);
runTouchScrollMomentum();
} else {
console.log(\`[Scroll Deep] 🛑 Pointer 静止释放或非滚动释放\`);
}
`;
// Now apply pointer changes
code = code.replace(/onTouchKeyboardPointerDown = \(event: PointerEvent\) => \{[\s\S]*?if \(sessionStore\.state \!\=\= "connected"\) \{\n\s*return;\n\s*\}\n\s*\};/m, pointerDownCode);
code = code.replace(/onTouchKeyboardPointerMove = \(event: PointerEvent\) => \{[\s\S]*?event\.stopImmediatePropagation\(\);\n\s*\};/m, pointerMoveCode);
code = code.replace(/onTouchKeyboardPointerUp = \(event: PointerEvent\) => \{[\s\S]*?if \(event\.pointerType \!\=\= "touch" \|\| touchGatePointerId \!\=\= event.pointerId\) \{\n\s*return;\n\s*\}/m, pointerUpCode);
fs.writeFileSync('src/components/TerminalPanel.vue', code);

View File

@@ -0,0 +1,8 @@
const fs = require('fs');
let code = fs.readFileSync('src/components/TerminalPanel.vue', 'utf8');
const badPattern = /\/\/ event\.stopImmediatePropagation\(\);\n\s*if \(event\.cancelable\)\s*\{\n\s*event\.preventDefault\(\); \/\/ 我们使用 JS 控制滚动,必须禁止系统原生滚动争抢\n\s*\}/g;
code = code.replace(badPattern, 'event.stopImmediatePropagation();');
fs.writeFileSync('src/components/TerminalPanel.vue', code);

View File

@@ -0,0 +1,9 @@
const fs = require('fs');
let code = fs.readFileSync('src/components/TerminalPanel.vue', 'utf8');
// I commented out all stopImmediatePropagation! That's bad for click/native selection.
// Let's put back ONLY the one in stopImmediatePropagation for PASS_NATIVE etc, but leave touchmove alone.
// Actually I just use regex to revert everything and do it right.
code = code.replace(/\/\/ event\.stopImmediatePropagation\(\);/g, 'event.stopImmediatePropagation();');
fs.writeFileSync('src/components/TerminalPanel.vue', code);

View File

@@ -0,0 +1,9 @@
const fs = require('fs');
let code = fs.readFileSync('src/styles/main.css', 'utf8');
code = code.replace(/\.xterm-screen {\n touch-action: none !important;\n}\n\n\n/g, '');
code = code.replace(/\.xterm-text-layer {\n touch-action: none !important;\n}\n\n\n/g, '');
code = code.replace(/\.xterm-rows {\n touch-action: none !important;\n}\n/g, '');
code = code.replace(/touch-action: none !important;\n touch-action: none !important;\n/g, '');
fs.writeFileSync('src/styles/main.css', code);

View File

@@ -0,0 +1,8 @@
const fs = require('fs');
let code = fs.readFileSync('src/styles/main.css', 'utf8');
// I put touch-action on xterm-viewport... but wait, the screen you drag on is the `xterm-screen` canvas!
// We need touch-action: none on .xterm-screen, .xterm-text-layer, and .xterm-viewport.
code += '\n\n.xterm-rows {\n touch-action: none !important;\n}\n';
fs.writeFileSync('src/styles/main.css', code);

View File

@@ -0,0 +1,13 @@
const fs = require('fs');
let code = fs.readFileSync('src/styles/main.css', 'utf8');
// The reason touchmove stops is because touch events on iOS are sometimes silently swallowed if touch-action is not correct.
// Actually, earlier you saw the logs: only `pointermove` is logging!! The `touchmove` ONLY LOGS ONCE!
// Safari 13+ on iOS stops emitting touchmove if it emits pointermove, UNLESS we call e.preventDefault() on the touchmove, but since we didn't get touchmove... Wait.
// Safari emits touchstart -> pointerdown -> touchmove (if not scrolling) / pointermove.
// Actually, the iOS browser decides it's doing a native scroll, so it cancels the `touchmove`. But wait, there was NO `touchcancel`!
// What if iOS is using `pointermove` instead of `touchmove` entirely for the rest of the gesture?
// YES! Pointer events are replacing Touch events in Safari for some logic!
// Our `touchmove` handler was the only one syncing the scroll:
// \`onTouchKeyboardTouchMove = (event: TouchEvent) => { ... }\`
// We IGNORED pointermove!

View File

@@ -0,0 +1,27 @@
const fs = require('fs');
let code = fs.readFileSync('src/components/TerminalPanel.vue', 'utf8');
// I need to ensure touchmove event preventDefault is working nicely WITH touch-action: none.
// In iOS Safari, if touch-action is none, you often don't even need preventDefault for scrolling, but since xterm is a beast, we need to completely bypass it.
// Let's rewrite the touchmove prevent default logic:
// Sometimes stopImmediatePropagation stops our own handlers if there are others attached manually to the DOM.
// Since xterm also attaches to `.xterm-viewport` and `.xterm-screen`, we use the capture phase on `containerRef.value`.
// The issue: "手指一直滑动,但屏幕没有滚动 但也没有触发 touchcancel"
// Wait! If there is no touchcancel and no touchend... how did `pointerup` trigger?
// Because pointer events are synthesized by iOS Safari!
// In Safari, if a pointer starts but the system decides it is a scroll, it CANCELS the pointer (pointercancel).
// But here, we got POINTER UP. This means Safari thought it was a click/drag, NOT a system scroll!
// But why did touchmove stop firing?
// In iOS, if you do `preventDefault()` on `touchstart` or `touchmove`, it disables system scrolling and gives you ALL `touchmove` events.
// Let's check `touchstart`. We did NOT call `preventDefault()` on `touchstart`.
// Let's call `preventDefault()` on `touchstart` when `touchGateHadSelectionAtStart` is false!
code = code.replace(/onTouchKeyboardTouchStart = \(event: TouchEvent\) => \{/, `onTouchKeyboardTouchStart = (event: TouchEvent) => {
// 必须阻止 touchstart 默认行为,否则 iOS 会尝试将其识别为原生手势而截断后续 touchmove
if (event.cancelable && !hasActiveNativeSelectionInTerminal()) {
event.preventDefault();
}`);
fs.writeFileSync('src/components/TerminalPanel.vue', code);

View File

@@ -0,0 +1,8 @@
const fs = require('fs');
let code = fs.readFileSync('src/components/TerminalPanel.vue', 'utf8');
// The touchmove event on containerRef is currently { capture: true, passive: true }.
// xterm installs its own touchmove listener on the terminal wrapper but if we don't interfere, it should just work.
// Wait, xterm DOES its own momentum? No, xterm doesn't have mobile momentum out of the box... wait, maybe it does?
console.log('Cleaned up TouchMove. Testing theory.');

View File

@@ -0,0 +1,7 @@
const fs = require('fs');
let code = fs.readFileSync('src/components/TerminalPanel.vue', 'utf8');
// The issue might be pointermove. If we are completely hands-off, we must also make sure pointermove is not stopping it.
const regex = /onTouchKeyboardPointerMove = \(event: PointerEvent\) => \{[\s\S]*?touchGateScrollLike = true;\n\s*\}/;
console.log(regex.test(code));

View File

@@ -0,0 +1,9 @@
const fs = require('fs');
let code = fs.readFileSync('src/components/TerminalPanel.vue', 'utf8');
// wait, how is xterm hooking into scrolling on mobile if we do stopImmediatePropagation on touchmove?
// Actually, if we do stopImmediatePropagation, xterm DOES NOT see the touchmove. So xterm DOES NOT scroll the viewport.
// And because the .xterm-screen (where the touch starts) DOES NOT have overflow: scroll, the browser DOES NOT scroll the viewport natively either!!
// This explains why it's completely frozen! We stopped xterm from scrolling it, AND the browser doesn't scroll it natively because the touch target isn't the scrollable container.
console.log("Found the core issue.");

View File

@@ -0,0 +1,8 @@
const fs = require('fs');
let code = fs.readFileSync('src/components/TerminalPanel.vue', 'utf8');
code = code.replace(/viewportScroller\.addEventListener\(type, /g, 'viewportScroller!.addEventListener(type, ');
code = code.replace(/const threshold = 0\.2;/g, 'const threshold_v = 0.2;');
code = code.replace(/threshold\)/g, 'threshold_v)');
fs.writeFileSync('src/components/TerminalPanel.vue', code);

View File

@@ -0,0 +1,8 @@
const fs = require('fs');
let code = fs.readFileSync('src/components/TerminalPanel.vue', 'utf8');
code = code.replace(/const threshold_v = 0\.2;/g, '');
code = code.replace(/threshold_v/g, '0.2');
code = code.replace(/Math\.abs\(touchScrollVelocity\) > threshold && /g, 'Math.abs(touchScrollVelocity) > 0.2 && ');
fs.writeFileSync('src/components/TerminalPanel.vue', code);

View File

@@ -0,0 +1,64 @@
const fs = require('fs');
let code = fs.readFileSync('src/components/TerminalPanel.vue', 'utf8');
// Inside `onTouchKeyboardPointerMove`: we can perform the actual scrolling!
const pointerMoveRegex = /onTouchKeyboardPointerMove = \(event: PointerEvent\) => \{[\s\S]*?touchGateScrollLike = true;\n\s*\}/m;
// Let's replace the whole onTouchKeyboardPointerMove function to sync scrolling instead of just detecting it.
// Actually, earlier the user showed that the `touchmove` stopped. Why did `touchmove` stop?
// Because we have `touch-action: none`?
// iOS Safari requires `e.preventDefault()` on `touchmove` to keep the `touchmove` stream alive, but you have to do it BEFORE the compositor steals it.
// We added: `if (event.cancelable && !hasActiveNativeSelectionInTerminal()) { event.preventDefault(); }` to `touchstart`!
// If we call preventDefault on touchstart, NO POINTER EVENTS or touch events should be stolen by scroll. BUT touchmove STILL DISAPPEARED?
// Wait, if we call preventDefault on touchstart, does that disable pointermove? Or touchmove?
// Actually if `touch-action: none` is present, pointer events fire constantly, while touch events MIGHT ONLY FIRE IF POINTER EVENTS DON'T CANCEL THEM.
code = code.replace(/onTouchKeyboardPointerMove = \(event: PointerEvent\) => \{[\s\S]*?\}\;\n onTouchKeyboardPointerUp/m,
` onTouchKeyboardPointerMove = (event: PointerEvent) => {
if (event.pointerType !== "touch" || touchGatePointerId !== event.pointerId) {
return;
}
const dx = event.clientX - touchGateStartX;
const dy = event.clientY - touchGateStartY;
const absDx = Math.abs(dx);
const absDy = Math.abs(dy);
if (absDx > TOUCH_KEYBOARD_TAP_MAX_MOVE_PX || absDy > TOUCH_KEYBOARD_TAP_MAX_MOVE_PX) {
touchGateMoved = true;
}
if (absDy > absDx && absDy > TOUCH_KEYBOARD_TAP_MAX_MOVE_PX) {
touchGateScrollLike = true;
}
// --- Pointer Scroll Sync ---
const hasSelection = hasActiveNativeSelectionInTerminal();
if (!hasSelection && viewportScroller) {
// Stop system gestures like swipe back if needed
event.stopImmediatePropagation();
const now = performance.now();
const dt = now - touchScrollLastTime;
const currentY = event.clientY;
const deltaY = currentY - (touchScrollLastY || currentY);
if (dt > 0 && touchScrollLastTime > 0) {
viewportScroller.scrollTop -= deltaY;
const v = (-deltaY / dt) * 16;
if (touchScrollVelocity * v > 0) {
touchScrollVelocity = touchScrollVelocity * 0.5 + v * 0.5;
} else {
touchScrollVelocity = v;
}
const touchMaxSpeed = 120;
if (touchScrollVelocity > touchMaxSpeed) touchScrollVelocity = touchMaxSpeed;
else if (touchScrollVelocity < -touchMaxSpeed) touchScrollVelocity = -touchMaxSpeed;
}
touchScrollLastY = currentY;
touchScrollLastTime = now;
}
};
onTouchKeyboardPointerUp`);
fs.writeFileSync('src/components/TerminalPanel.vue', code);

View File

@@ -0,0 +1,8 @@
const fs = require('fs');
let code = fs.readFileSync('src/components/TerminalPanel.vue', 'utf8');
// To let xterm's native mobile JS scroll work, we MUST NOT intercept touchmove with stopImmediatePropagation. We must pass the events cleanly to xterm.
const regexTouchMove = /onTouchKeyboardTouchMove = \(event: TouchEvent\) => \{[\s\S]*?event\.stopImmediatePropagation\(\);\n\s*\};/;
code = code.replace(regexTouchMove, `onTouchKeyboardTouchMove = (event: TouchEvent) => {\n // Let xterm handle the touchmove natively! Do not stop propagation. It needs to calculate delta and apply it to viewportScroller.\n };`);
fs.writeFileSync('src/components/TerminalPanel.vue', code);

View File

@@ -0,0 +1,106 @@
const fs = require('fs');
let code = fs.readFileSync('src/components/TerminalPanel.vue', 'utf8');
// Replace standard touch handlers to heavily trace
let newStart = ` onTouchKeyboardTouchStart = (event: TouchEvent) => {
clearTouchScrollMomentum();
if (event.touches.length > 0) {
touchScrollLastY = event.touches[0]?.clientY || 0;
touchScrollLastTime = performance.now();
touchScrollVelocity = 0;
}
console.log("[Scroll Deep] 🟢 TOUCH START:", {
touches: event.touches.length,
isCancelable: event.cancelable,
target: event.target?.nodeName,
viewportScrollTop: viewportScroller?.scrollTop
});
if (!helperTextarea) return;
if (focusKeyboardTimerId !== null) {
window.clearTimeout(focusKeyboardTimerId);
focusKeyboardTimerId = null;
clearFocusKeyboardBlurRecover();
focusKeyboardInProgress = false;
}
helperTextarea.readOnly = true;
};`;
let newMove = ` onTouchKeyboardTouchMove = (event: TouchEvent) => {
console.log(\`[Scroll Deep] 🔵 TOUCH MOVE | cancelable: \${event.cancelable} | target: \${event.target?.nodeName}\`);
const hasSelection = hasActiveNativeSelectionInTerminal();
if (hasSelection) {
lastTouchAction = "PASS_NATIVE";
event.stopImmediatePropagation();
return;
}
const now = performance.now();
const dt = now - touchScrollLastTime;
const currentY = event.touches[0]?.clientY || 0;
const dy = currentY - touchScrollLastY;
// Stop xterm from intercepting this manually
event.stopImmediatePropagation();
if (viewportScroller && dt > 0) {
const before = viewportScroller.scrollTop;
if (dy !== 0) {
// JS 强控滑动:完全跟手,零延迟
viewportScroller.scrollTop -= dy;
}
// 计算物理滑动速度,用于释放后的动量
const v = (-dy / dt) * 16;
if (touchScrollVelocity * v > 0) {
touchScrollVelocity = touchScrollVelocity * 0.5 + v * 0.5;
} else {
touchScrollVelocity = v;
}
const touchMaxSpeed = 120;
if (touchScrollVelocity > touchMaxSpeed) touchScrollVelocity = touchMaxSpeed;
else if (touchScrollVelocity < -touchMaxSpeed) touchScrollVelocity = -touchMaxSpeed;
if (Math.abs(dy) > 2) {
console.log(\`[Scroll Deep] ➡️ 手指位移真实触发生效Δy=\${dy.toFixed(1)}px | 渲染视口=\${before}→\${viewportScroller.scrollTop} | v=\${v.toFixed(2)}\`);
}
}
touchScrollLastY = currentY;
touchScrollLastTime = now;
};`;
let newEnd = ` onTouchKeyboardTouchEnd = (event: TouchEvent) => {
console.log(\`[Scroll Deep] 🔴 TOUCH END/CANCEL | cancelable: \${event.cancelable} | target: \${event.target?.nodeName} | 最终 scrollTop: \${viewportScroller?.scrollTop}\`);
const threshold = 0.2;
if (Math.abs(touchScrollVelocity) > threshold) {
touchScrollVelocity *= 1.35;
console.log(\`[Scroll Deep] 🚀 准备进行 JS 惯性滚动,起步速度=\${touchScrollVelocity.toFixed(2)}\`);
runTouchScrollMomentum();
} else {
console.log(\`[Scroll Deep] 🛑 静止释放,不触发惯性\`);
}
};`;
code = code.replace(/onTouchKeyboardTouchStart = \(event: TouchEvent\) => \{[\s\S]*?\}\;\n onTouchKeyboardTouchMove = \(event: TouchEvent\) => \{[\s\S]*?\}\;\n onTouchKeyboardTouchEnd = \(\) => \{[\s\S]*?\}\;/m,
newStart + '\n' + newMove + '\n' + newEnd);
// Add pointer event logs
code = code.replace(/onTouchKeyboardPointerDown = \(event: PointerEvent\) => \{/, `onTouchKeyboardPointerDown = (event: PointerEvent) => {
console.log("[Scroll Deep] 🟡 POINTER DOWN: ", { pointerId: event.pointerId, type: event.pointerType, target: event.target?.nodeName });`);
code = code.replace(/onTouchKeyboardPointerMove = \(event: PointerEvent\) => \{/, `onTouchKeyboardPointerMove = (event: PointerEvent) => {
if (event.pointerType === "touch" && touchGatePointerId === event.pointerId) {
// only log occasionally to avoid spam, we are heavily logging touchmove already
}`);
code = code.replace(/onTouchKeyboardPointerUp = \(event: PointerEvent\) => \{/, `onTouchKeyboardPointerUp = (event: PointerEvent) => {
console.log("[Scroll Deep] 🟠 POINTER UP: ", { pointerId: event.pointerId, type: event.pointerType });`);
code = code.replace(/onTouchKeyboardPointerCancel = \(event: PointerEvent\) => \{/, `onTouchKeyboardPointerCancel = (event: PointerEvent) => {
console.log("[Scroll Deep] ⚫ POINTER CANCEL: ", { pointerId: event.pointerId, type: event.pointerType });`);
fs.writeFileSync('src/components/TerminalPanel.vue', code);

View File

@@ -0,0 +1,13 @@
const fs = require('fs');
let code = fs.readFileSync('src/components/TerminalPanel.vue', 'utf8');
// The original xterm might be intercepting touchstart to call preventDefault? No, xterm doesn't do that.
// What about the terminal's wheel? xterm handles wheel.
// The issue might be that pointer events are taking priority and scrolling text selection instead of the scroll pane.
// Re-enable ALL events with very clear logs.
code = code.replace(/containerRef\.value\.addEventListener\("touchmove", onTouchKeyboardTouchMove, \{ capture: true, passive: true \}\);/,
'containerRef.value.addEventListener("touchmove", onTouchKeyboardTouchMove, { capture: true, passive: false });');
fs.writeFileSync('src/components/TerminalPanel.vue', code);

View File

@@ -0,0 +1,24 @@
const fs = require('fs');
let code = fs.readFileSync('src/components/TerminalPanel.vue', 'utf8');
// The reason it fired PointerUp might be because iOS triggered a "click" action or lost touch tracking.
// Since we used e.preventDefault() in touchmove, Safari DOES NOT cancel the pointer if it thinks we are handling panning.
// BUT Safari needs `touch-action: none` on the container, otherwise it might intercept the gesture asynchronously, which sometimes leads to lost events.
// Wait, if it intercepted, it fires touchcancel.
// Let's modify the touchListeners to use non-passive on touchstart too!
code = code.replace(/containerRef.value.addEventListener\("touchstart", onTouchKeyboardTouchStart, \{ capture: true, passive: true \}\);/g,
'containerRef.value.addEventListener("touchstart", onTouchKeyboardTouchStart, { capture: true, passive: false });');
// Actually, in CSS, `touch-action: none` completely disables native panning, giving JS 100% control of touchmove. IF text selection requires panning, then we should use `touch-action: pan-x pan-y`, but `none` solves weird touch interruptions.
// Let's modify touchstart to preventDefault if it's strictly a scroll gesture? No, touchstart doesn't know it's a scroll yet.
// Let's look at pointermove:
// xterm literally calls `event.preventDefault()` on touchmove internally IF IT IS NOT PASSIVE.
// BUT we intercepted and called stopImmediatePropagation, so xterm never sees it.
// If the user says "手指一直滑动,但屏幕没有滚动", and NO MOVES LOGGED AFTER THE FIRST ONE, it means the browser killed the JS event stream because it decided to take over the pan, OR it fired touchend immediately. (The log shows Pointer Up... did Touch End log?)
// Wait! There is NO Touch End log in the user's snippet!
// "🟡 POINTER DOWN" -> "🟢 TOUCH START" -> "🔵 TOUCH MOVE" -> "➡️ 手指位移" -> "🟠 POINTER UP"
// ONLY Pointer Up fired! Touch End and Touch Cancel NEVER fired? Or maybe they were missed? No, the user would provide them.
// Why would Pointer Up fire without Touch End? iOS Safari sometimes drops touch events in favor of pointer events if scrolling initiates.

View File

@@ -0,0 +1,11 @@
const fs = require('fs');
let code = fs.readFileSync('src/components/TerminalPanel.vue', 'utf8');
// Inside touchmove, add e.preventDefault() if it is cancelable
code = code.replace(/event\.stopImmediatePropagation\(\);/g,
`event.stopImmediatePropagation();
if (event.cancelable) {
event.preventDefault(); // 我们使用 JS 控制滚动,必须禁止系统原生滚动争抢
}`);
fs.writeFileSync('src/components/TerminalPanel.vue', code);

View File

@@ -0,0 +1,18 @@
const fs = require('fs');
let code = fs.readFileSync('src/components/TerminalPanel.vue', 'utf8');
// The issue: "手指一直滑动,但屏幕没有滚动. 似乎无效的事件并没有被捕捉到"
// AND we only saw ONE touchmove!
// WAIT! "🟠 POINTER UP" fired immediately!
// This means the OS literally cancelled the touch gesture because it thinks it became a native scrolling gesture!!
// In iOS Safari, if you don't call preventDefault() on BOTH `touchstart` AND `touchmove` IMMEDIATELY, the browser takes over the touch payload, makes it a pan/scroll gesture, sending a `pointercancel` or `pointerup`, and STOPS firing `touchmove` to JS!
// But wait, `touchstart` is marked as { passive: true }. iOS is seizing the pan gesture.
// What if we try to switch back to letting xterm do its own DOM scroll but with proper CSS config?
// No, the user explicitly asked NOT to use xterm's code and that xterm hijacked it.
// Let's modify touchstart to be non-passive, or at least understand why pointerUp happened.
// "POINTER UP" fired instead of "POINTER CANCEL". If it was a system pan, it would be pointerCancel or touchCancel. pointerUp means physical lift? But user says "手指一直滑动". So maybe user slid, but the touch tracker ended?
// Actually, no, if it's long press native selection, pointerUp.
// Wait! `absDy > TOUCH_KEYBOARD_TAP_MAX_MOVE_PX` -> `event.stopImmediatePropagation()` on pointermove!
// Oh! Does our `pointermove` prevent `touchmove` from firing if pointer events are capturing it?
// No, DOM standard dictates both fire. But in Safari, touch and pointer events interact weirdly.

View File

@@ -0,0 +1,27 @@
const fs = require('fs');
let code = fs.readFileSync('src/components/TerminalPanel.vue', 'utf8');
// The reason it's stuttering and freezing might be related to CSS native scrolling fighting with xterm's pointer/touch handlers, or our own JS momentum not playing well.
// But wait, the logs showed Δy is HUGE (e.g. 102px in 16ms -> speed -102).
// And the `viewportScroller.scrollTop` was actually updating fine, BUT xterm might not be syncing its rows because xterm intercepts `wheel` and `touchmove` originally to sync `core._core.viewport.scroll`.
// Wait... if we update `.xterm-viewport`'s `scrollTop` manually, does xterm re-render the canvas/dom lines?
// YES it does through the `scroll` event listener on '.xterm-viewport'.
// Let's completely nuke `pointermove` preventions and xterm interception during passive scrolls, and let the BROWSER do EVERYTHING natively.
// The user noted that when "现在手指滑动失效" (scrolling broke altogether), it was when we removed EventListeners completely. Because xterm sets up the viewport explicitly.
// Let's revert touchmove and touchstart, and pointermove to just STOP PROPAGATION but NOT prevent default, and importantly, REMOVE our JS scroll tracking since native scroll is just better. Wait. The user's logs WERE our JS scrolling!
// Look at the logs:
// [Log] [Scroll] 🖐️ MOVE | 跟手延迟Δt=1.0ms | 手指位移Δy=50.7px | 渲染视口=3896→3845 | 当前算得速度v=-810.67
// The browser JS engine is grouping events. 1.0ms and jumping 50.7px. This means it's a huge jump between RAF frames.
// JS-driven scrolling via touchmove on mobile is ALWAYS janky because `touchmove` frequency is lower and uneven compared to compositor scrolling.
// What we MUST DO:
// 1. Let browser natively scroll the container (.xterm-viewport has overflow-y: auto)
// 2. STOP xterm from intercepting touchmove so it doesn't call e.preventDefault().
// 3. We do NOT update scrollTop manually.
// 4. We DO use pointers / touches.
// Wait, I already did this! Remember, user said "现在不动" (it doesn't move now) when I just did stopImmediatePropagation.
// Why did it not move? Because `.xterm-viewport` DOES NOT HAVE CSS overflow: auto?
// Actually I removed my CSS changes to `main.css` earlier!

View File

@@ -0,0 +1,93 @@
const fs = require('fs');
let code = fs.readFileSync('src/components/TerminalPanel.vue', 'utf8');
// Replace standard touch handlers to use JS custom scrolling with deep logging
let newStart = ` onTouchKeyboardTouchStart = (event: TouchEvent) => {
clearTouchScrollMomentum();
if (event.touches.length > 0) {
touchScrollLastY = event.touches[0]?.clientY || 0;
touchScrollLastTime = performance.now();
touchScrollVelocity = 0;
}
console.log("[Scroll] 👆 Touch START:", {
y: touchScrollLastY,
time: touchScrollLastTime.toFixed(1),
viewportScrollTop: viewportScroller?.scrollTop
});
if (!helperTextarea) return;
if (focusKeyboardTimerId !== null) {
window.clearTimeout(focusKeyboardTimerId);
focusKeyboardTimerId = null;
clearFocusKeyboardBlurRecover();
focusKeyboardInProgress = false;
}
helperTextarea.readOnly = true;
if (DEBUG_TOUCH_FOCUS) {
console.log("[TouchFocus] touchstart → readOnly=true (no blur)");
}
};`;
let newMove = ` onTouchKeyboardTouchMove = (event: TouchEvent) => {
const hasSelection = hasActiveNativeSelectionInTerminal();
if (hasSelection) {
lastTouchAction = "PASS_NATIVE";
event.stopImmediatePropagation();
return;
}
const now = performance.now();
const dt = now - touchScrollLastTime;
const currentY = event.touches[0]?.clientY || 0;
const dy = currentY - touchScrollLastY;
// Stop xterm from intercepting this manually
event.stopImmediatePropagation();
if (viewportScroller && dt > 0) {
const before = viewportScroller.scrollTop;
if (dy !== 0) {
// JS 强控滑动:完全跟手,零延迟
viewportScroller.scrollTop -= dy;
}
// 计算物理滑动速度,用于释放后的动量
const v = (-dy / dt) * 16;
if (touchScrollVelocity * v > 0) {
touchScrollVelocity = touchScrollVelocity * 0.5 + v * 0.5;
} else {
touchScrollVelocity = v;
}
// 限制最大动量
const touchMaxSpeed = 120; // 匹配原本常量
if (touchScrollVelocity > touchMaxSpeed) touchScrollVelocity = touchMaxSpeed;
else if (touchScrollVelocity < -touchMaxSpeed) touchScrollVelocity = -touchMaxSpeed;
console.log(\`[Scroll] 🖐️ MOVE | 跟手延迟Δt=\${dt.toFixed(1)}ms | 手指位移Δy=\${dy.toFixed(1)}px | 渲染视口=\${before}→\${viewportScroller.scrollTop} | 当前算得速度v=\${v.toFixed(2)}\`);
}
touchScrollLastY = currentY;
touchScrollLastTime = now;
};`;
let newEnd = ` onTouchKeyboardTouchEnd = () => {
console.log(\`[Scroll] 👇 Touch END | 释放时瞬时速度=\${touchScrollVelocity.toFixed(2)} | 最终 scrollTop: \${viewportScroller?.scrollTop}\`);
// 添加阈值,防止极轻微误触引发滚动
const threshold = 0.2;
if (Math.abs(touchScrollVelocity) > threshold) {
// 轻微乘子放大,提升顺滑手感
touchScrollVelocity *= 1.35;
console.log(\`[Scroll] 🚀 触发惯性滚动,起步速度=\${touchScrollVelocity.toFixed(2)}\`);
runTouchScrollMomentum();
} else {
console.log(\`[Scroll] 🛑 速度太小(\${touchScrollVelocity.toFixed(2)}),不触发惯性\`);
}
};`;
code = code.replace(/onTouchKeyboardTouchStart = \(event: TouchEvent\) => \{[\s\S]*?\}\;\n onTouchKeyboardTouchMove = \(event: TouchEvent\) => \{[\s\S]*?\}\;\n onTouchKeyboardTouchEnd = \(\) => \{[\s\S]*?\}\;/m,
newStart + '\n' + newMove + '\n' + newEnd);
fs.writeFileSync('src/components/TerminalPanel.vue', code);

View File

@@ -0,0 +1,29 @@
const fs = require('fs');
let code = fs.readFileSync('src/components/TerminalPanel.vue', 'utf8');
// Remove variables
code = code.replace(/let touchScrollVelocity = 0;\n?/g, '');
code = code.replace(/let touchScrollRaf:\s*number\s*\|\s*null = null;\n?/g, '');
// Remove clearTouchScrollMomentum definition
code = code.replace(/function clearTouchScrollMomentum\(\): void \{[\s\S]*?\}\n/g, '');
// Remove runTouchScrollMomentum definition
code = code.replace(/function runTouchScrollMomentum\(\): void \{[\s\S]*?\}\n/g, '');
// Remove velocity tracking in pointermove
code = code.replace(/\s*const v = \(\-deltaY \/ dt\) \* 16;[\s\S]*?if \(touchScrollVelocity > touchMaxSpeed\) touchScrollVelocity = touchMaxSpeed;/g, '');
// Remove velocity tracking in touchmove
code = code.replace(/\s*\/\/ 计算物理滑动速度,用于释放后的动量[\s\S]*?\n\s*if \(Math\.abs\(dy\) > 2\) \{/g, '\n if (Math.abs(dy) > 2) {');
// Remove velocity call in touchend
code = code.replace(/\s*if \(Math\.abs\(touchScrollVelocity\) > 0\.2\) \{[\s\S]*?\n\s*\}/g, '');
// Remove velocity call in pointerup
code = code.replace(/\s*\/\/ 释放时触发滚行动量\s*if \(Math\.abs\(touchScrollVelocity\) > 0\.2 && touchGateScrollLike && !hasActiveNativeSelectionInTerminal\(\)\) \{[\s\S]*?\n\s*\}/g, '');
// Clean calls to clearTouchScrollMomentum
code = code.replace(/\s*clearTouchScrollMomentum\(\);/g, '');
fs.writeFileSync('src/components/TerminalPanel.vue', code);

View File

@@ -0,0 +1,13 @@
const fs = require('fs');
let code = fs.readFileSync('src/components/TerminalPanel.vue', 'utf8');
// Also remove residual bits
code = code.replace(/\s*touchScrollVelocity = 0;/g, '');
code = code.replace(/\s*touchScrollVelocity \*= TOUCH_SCROLL_DAMPING;/g, '');
code = code.replace(/\s*if \(Math\.abs\(touchScrollVelocity\) < TOUCH_SCROLL_STOP_THRESHOLD\) \{[\s\S]*?\}\n/g, '');
code = code.replace(/\s*viewportScroller\.scrollTop \+= touchScrollVelocity;/g, '');
code = code.replace(/function clearTouchScrollMomentum[\s\S]*?\n\}\n/g, '');
code = code.replace(/function runTouchScrollMomentum[\s\S]*?\n\}\n/g, '');
fs.writeFileSync('src/components/TerminalPanel.vue', code);

View File

@@ -0,0 +1,19 @@
const fs = require('fs');
let code = fs.readFileSync('src/components/TerminalPanel.vue', 'utf8');
// clean global intercept block if it exists
code = code.replace(/\/\/ \-\-\- 极致全局监听器[\s\S]*?\n\s*\}\);\n\n/g, '');
// remove multiline logs
code = code.replace(/\s*console\.log\(\s*(?:`|"|')\[Scroll Deep\][\s\S]*?\);\n/g, '\n');
// remove empty else blocks
code = code.replace(/\s*\} else \{\s*\n\s*\}/g, '}');
// remove duplicated momentum logic in pointerup
code = code.replace(/(\s*\/\/ 释放时触发滚行动量\s*if \(Math\.abs\(touchScrollVelocity\) > 0\.2 && touchGateScrollLike && !hasActiveNativeSelectionInTerminal\(\)\) \{\s*touchScrollVelocity \*= 1\.35;\s*runTouchScrollMomentum\(\);\s*\})\s*\/\/ 释放时触发滚行动量\s*if \(Math\.abs\(touchScrollVelocity\) > 0\.2 && touchGateScrollLike && !hasActiveNativeSelectionInTerminal\(\)\) \{\s*touchScrollVelocity \*= 1\.35;\s*runTouchScrollMomentum\(\);\s*\}/g, '$1');
// fix the syntax bug around touch end momentum
code = code.replace(/\s*if \(Math\.abs\(touchScrollVelocity\) > 0\.2\) \{\s*touchScrollVelocity \*= 1\.35;\s*runTouchScrollMomentum\(\);\s*\};\s*\n/g, '\n if (Math.abs(touchScrollVelocity) > 0.2) {\n touchScrollVelocity *= 1.35;\n runTouchScrollMomentum();\n }\n');
fs.writeFileSync('src/components/TerminalPanel.vue', code);

View File

@@ -0,0 +1,23 @@
const fs = require('fs');
let code = fs.readFileSync('src/components/TerminalPanel.vue', 'utf8');
const globalTracker = `
// --- 极致全局监听器,抓出是谁吃掉了 move ---
const globalEventDebug = (e: Event) => {
// 若这是消失的 move 事件,打出到底在哪被拦截了
if (e.type === "touchmove" || e.type === "pointermove" || e.type === "touchcancel" || e.type === "pointercancel") {
// 为了不刷屏,如果是 pointermove 我们只在 touch 活跃时打印
if (e.type === "pointermove" && (e as PointerEvent).pointerType !== "touch") return;
console.log(\`[GLOBAL INTERCEPT] \${e.type} | Phase: \${e.eventPhase} | Cancelable: \${e.cancelable} | Target: \${(e.target as Element)?.className || (e.target as Element)?.tagName}\`);
}
};
['touchmove', 'touchstart', 'touchend', 'touchcancel', 'pointerdown', 'pointermove', 'pointerup', 'pointercancel'].forEach(type => {
// 用最顶级的 window capture 拦截,这样在任何人(包括 xterm 内部那些黑盒代码)前面执行
window.addEventListener(type, globalEventDebug, { capture: true, passive: false });
});
`;
code = code.replace(/function initTerminal\(\)\: void \{/, `function initTerminal(): void {\n${globalTracker}`);
fs.writeFileSync('src/components/TerminalPanel.vue', code);

View File

@@ -0,0 +1,17 @@
const fs = require('fs');
let code = fs.readFileSync('src/components/TerminalPanel.vue', 'utf8');
const regexTouchMove = /onTouchKeyboardTouchMove = \(event: TouchEvent\) => \{[\s\S]*?touchScrollLastTime = now;\n\s*\};/g;
const newTouchMove = `onTouchKeyboardTouchMove = (event: TouchEvent) => {
// 通过 stopImmediatePropagation 拦截 xterm 内部那些“接管浏览器滚动”的事件处理器。
// 但绝对不要自己掉用 event.preventDefault(),从而把物理滚动和动量动画原模原样还给浏览器与操作系统。
event.stopImmediatePropagation();
};`;
code = code.replace(regexTouchMove, newTouchMove);
// Also remove touchstart redundant vars
code = code.replace(/touchScrollLastY = event\.touches\[0\]\?\.clientY \|\| 0;\n\s*touchScrollLastTime = performance\.now\(\);\n/g, '');
fs.writeFileSync('src/components/TerminalPanel.vue', code);