Files
NetMesh/scripts/patch-xterm-macos-column-selection.cjs
zhaolei 3c72efcb7f
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
[Init] Initial commit - NetMesh terminal manager
2026-09-13 18:24:01 +08:00

148 lines
5.5 KiB
JavaScript

#!/usr/bin/env node
/* global process, console */
"use strict";
const fs = require("node:fs");
const path = require("node:path");
const EXPECTED_XTERM_VERSION = "6.1.0-beta.292";
const FORCE_SELECTION_OPTION = "macOptionClickForcesSelection";
const META_OPTION = "macOptionIsMeta".padEnd(FORCE_SELECTION_OPTION.length, " ");
// xterm normally treats macOptionClickForcesSelection and column selection as
// mutually exclusive. Netcatty needs both when Option is not Meta: Option must
// keep forcing local selection inside mouse-aware programs, and that local
// selection must retain the standard macOS rectangular shape. Patch the pinned
// bundles at install time and fail closed if upstream changes the expected code
// shape.
const PATCHES = [
{
file: "node_modules/@xterm/xterm/lib/xterm.js",
original: "!(f.isMac&&this._optionsService.rawOptions.macOptionClickForcesSelection)",
replacement: `!(f.isMac&&this._optionsService.rawOptions.${META_OPTION})`,
preserveLength: true,
},
{
file: "node_modules/@xterm/xterm/lib/xterm.mjs",
original: "!(ie&&this._optionsService.rawOptions.macOptionClickForcesSelection)",
replacement: `!(ie&&this._optionsService.rawOptions.${META_OPTION})`,
preserveLength: true,
},
{
file: "node_modules/@xterm/xterm/src/browser/services/SelectionService.ts",
original: "return event.altKey && !(Browser.isMac && this._optionsService.rawOptions.macOptionClickForcesSelection);",
replacement: `return event.altKey && !(Browser.isMac && this._optionsService.rawOptions.${META_OPTION});`,
preserveStatementLength: true,
},
{
file: "node_modules/@xterm/xterm/lib/xterm.js.map",
original: "return event.altKey && !(Browser.isMac && this._optionsService.rawOptions.macOptionClickForcesSelection);",
replacement: `return event.altKey && !(Browser.isMac && this._optionsService.rawOptions.${META_OPTION});`,
preserveStatementLength: true,
},
{
file: "node_modules/@xterm/xterm/lib/xterm.mjs.map",
original: "return event.altKey && !(Browser.isMac && this._optionsService.rawOptions.macOptionClickForcesSelection);",
replacement: `return event.altKey && !(Browser.isMac && this._optionsService.rawOptions.${META_OPTION});`,
preserveStatementLength: true,
},
];
function sameLengthExpression(expression, length) {
if (expression.length > length) throw new Error("replacement expression is too long");
// Keep generated bundle offsets stable so the shipped source maps remain valid.
return `${expression}${" ".repeat(length - expression.length)}`;
}
function sameLengthStatement(statement, length) {
if (!statement.endsWith(";") || statement.length > length) {
throw new Error("replacement statement cannot preserve the target length");
}
return `${statement.slice(0, -1)}${" ".repeat(length - statement.length)};`;
}
function replacementForPatch(patch) {
if (!patch.replacement) throw new Error(`missing replacement for ${patch.file}`);
if (patch.preserveLength) {
return sameLengthExpression(patch.replacement, patch.original.length);
}
if (patch.preserveStatementLength) {
return sameLengthStatement(patch.replacement, patch.original.length);
}
return patch.replacement;
}
function patchXtermSource(source, patch) {
const replacement = replacementForPatch(patch);
const originalCount = source.split(patch.original).length - 1;
const patchedCount = source.split(replacement).length - 1;
if (originalCount === 1 && patchedCount === 0) {
return { source: source.replace(patch.original, replacement), changed: true };
}
if (originalCount === 0 && patchedCount === 1) {
return { source, changed: false };
}
throw new Error(
`${patch.file}: expected exactly one original or patched selection predicate ` +
`(original=${originalCount}, patched=${patchedCount})`,
);
}
/**
* @param {string} root
* @param {Pick<typeof fs, "rmSync">} fsImpl
*/
function invalidateViteCache(root = process.cwd(), fsImpl = fs) {
const cachePath = path.resolve(root, "node_modules/.vite");
fsImpl.rmSync(cachePath, { recursive: true, force: true });
return cachePath;
}
function patchInstalledXterm(root = process.cwd()) {
const packageJsonPath = path.resolve(root, "node_modules/@xterm/xterm/package.json");
const version = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")).version;
if (version !== EXPECTED_XTERM_VERSION) {
throw new Error(
`unsupported @xterm/xterm version ${version}; expected ${EXPECTED_XTERM_VERSION}`,
);
}
let changed = 0;
for (const patch of PATCHES) {
const absolutePath = path.resolve(root, patch.file);
const source = fs.readFileSync(absolutePath, "utf8");
const result = patchXtermSource(source, patch);
if (result.changed) {
fs.writeFileSync(absolutePath, result.source);
changed++;
}
}
const viteCachePath = invalidateViteCache(root);
return { changed, checked: PATCHES.length, version, viteCachePath };
}
if (require.main === module) {
try {
const result = patchInstalledXterm();
console.log(
`[patch-xterm-macos-column-selection] version=${result.version} ` +
`changed=${result.changed} checked=${result.checked} vite-cache=invalidated`,
);
} catch (error) {
console.error(`[patch-xterm-macos-column-selection] ERROR: ${error.message}`);
process.exitCode = 1;
}
}
module.exports = {
EXPECTED_XTERM_VERSION,
PATCHES,
invalidateViteCache,
patchInstalledXterm,
patchXtermSource,
replacementForPatch,
sameLengthExpression,
sameLengthStatement,
};