[Init] Initial commit - NetMesh terminal manager
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

This commit is contained in:
2026-09-13 18:24:01 +08:00
commit 3c72efcb7f
3255 changed files with 907009 additions and 0 deletions

View File

@@ -0,0 +1,427 @@
import assert from "node:assert/strict";
import { createRequire } from "node:module";
import test from "node:test";
import type { Terminal as XTermType } from "@xterm/xterm";
import {
getNormalizedTerminalSelection,
getTerminalSelectionForClipboard,
joinSoftWrappedRows,
trimWrittenPadding,
type SelectionBufferLine,
type SelectionTerminal,
} from "./normalizeTerminalSelection.ts";
const require = createRequire(import.meta.url);
const { Terminal: XTerm } = require("@xterm/xterm") as {
Terminal: typeof XTermType;
};
/**
* Fake line that matches real xterm translateToString(true) semantics:
* trimRight only drops *empty* cells (trailing chars that were never written),
* not written ASCII spaces used as display padding.
*/
function makeLine(
text: string,
options: {
isWrapped?: boolean;
emptyCells?: number;
cellWidths?: number[];
/**
* Model xterm.js 6 string-cache: a no-endColumn translateToString(true)
* drops written trailing spaces via String.trimEnd().
*/
canonicalTrimEnd?: boolean;
} = {},
): SelectionBufferLine {
const emptyCells = options.emptyCells ?? 0;
// Optional per-character terminal widths (default 1). Used to model CJK.
const widths = options.cellWidths ?? Array.from(text, () => 1);
let colLength = 0;
for (let i = 0; i < text.length; i += 1) {
colLength += widths[i] ?? 1;
}
const fullCols = colLength + emptyCells;
// Map each terminal column to a char (wide chars occupy two cols; second is spacer).
const cols: string[] = [];
for (let i = 0; i < text.length; i += 1) {
const ch = text[i]!;
const w = widths[i] ?? 1;
cols.push(ch);
for (let extra = 1; extra < w; extra += 1) {
cols.push(""); // wide-char continuation column
}
}
while (cols.length < fullCols) {
cols.push("\0");
}
return {
isWrapped: options.isWrapped ?? false,
length: fullCols,
getCell(x: number) {
if (x < 0 || x >= cols.length) return undefined;
const cell = cols[x];
if (cell === "\0" || cell === undefined) {
return { getChars: () => "", getCode: () => 0, getWidth: () => 1 };
}
if (cell === "") {
// Wide-character continuation column.
return { getChars: () => "", getCode: () => 0, getWidth: () => 0 };
}
const width = cell === "中" || /[\u3400-\u9fff]/u.test(cell) ? 2 : 1;
return {
getChars: () => cell,
getCode: () => cell.codePointAt(0) ?? 0,
getWidth: () => width,
};
},
translateToString(trimRight = false, startColumn = 0, endColumn?: number) {
const endSpecified = arguments.length >= 3;
let end = Math.max(startColumn, Math.min(endColumn ?? fullCols, fullCols));
if (trimRight) {
while (end > startColumn && (cols[end - 1] === "\0" || cols[end - 1] === "")) {
end -= 1;
}
}
let result = "";
for (let c = Math.max(0, startColumn); c < end; c += 1) {
const cell = cols[c];
if (cell && cell !== "\0") result += cell;
}
if (trimRight && options.canonicalTrimEnd && !endSpecified) {
return result.trimEnd();
}
return result;
},
};
}
function makeTerm(
lines: Array<{
text: string;
isWrapped?: boolean;
emptyCells?: number;
cellWidths?: number[];
canonicalTrimEnd?: boolean;
}>,
range: { start: { x: number; y: number }; end: { x: number; y: number } } | null,
options: {
rawSelection?: string;
columnSelect?: boolean;
} = {},
): SelectionTerminal {
const bufferLines = lines.map((line) =>
makeLine(line.text, {
isWrapped: line.isWrapped,
emptyCells: line.emptyCells,
cellWidths: line.cellWidths,
canonicalTrimEnd: line.canonicalTrimEnd,
}),
);
return {
getSelection: () => options.rawSelection ?? "",
getSelectionPosition: () => range,
buffer: {
active: {
getLine: (y) => bufferLines[y],
},
},
_core: options.columnSelect
? { _selectionService: { _activeSelectionMode: 3 } }
: { _selectionService: { _activeSelectionMode: 0 } },
};
}
test("trimWrittenPadding removes written trailing spaces but keeps internal ones", () => {
assert.equal(trimWrittenPadding("hello "), "hello");
assert.equal(trimWrittenPadding(" hello world "), " hello world");
});
test("joinSoftWrappedRows keeps a single trailing space as a word separator", () => {
assert.equal(joinSoftWrappedRows("hello ", "world"), "hello world");
});
test("joinSoftWrappedRows concatenates mid-word wraps tightly", () => {
assert.equal(joinSoftWrappedRows("hel", "lo world"), "hello world");
});
test("joinSoftWrappedRows does not invent spaces inside URL/path tokens", () => {
assert.equal(
joinSoftWrappedRows("https://example.com/very/long/ ", "path"),
"https://example.com/very/long/path",
);
});
test("joinSoftWrappedRows does not invent spaces before CJK punctuation", () => {
assert.equal(joinSoftWrappedRows("你好 ", ",世界"), "你好,世界");
});
test("joinSoftWrappedRows collapses multi-space prose padding to one space", () => {
// Pi/TUI word-wrapped prose: padding between words must not glue them.
assert.equal(joinSoftWrappedRows("the most ", "reliable"), "the most reliable");
assert.equal(joinSoftWrappedRows("shifts ", "across"), "shifts across");
});
test("joinSoftWrappedRows does not invent spaces between CJK characters", () => {
assert.equal(joinSoftWrappedRows("最 ", "稳"), "最稳");
// Single pad cell must not bypass the CJK check.
assert.equal(joinSoftWrappedRows("最 ", "稳"), "最稳");
assert.equal(joinSoftWrappedRows("한 ", "글"), "한글");
assert.equal(joinSoftWrappedRows("你好。 ", "世界"), "你好。世界");
});
test("joinSoftWrappedRows keeps a space after sentence-ending punctuation", () => {
assert.equal(
joinSoftWrappedRows("First sentence. ", "Next sentence"),
"First sentence. Next sentence",
);
});
test("joinSoftWrappedRows only uses the trailing token for URL detection", () => {
assert.equal(
joinSoftWrappedRows("See https://x.test for more ", "details"),
"See https://x.test for more details",
);
// Single trailing space after a URL is a real word separator.
assert.equal(
joinSoftWrappedRows("See https://example.com ", "today"),
"See https://example.com today",
);
});
test("joinSoftWrappedRows keeps path separators attached across wraps", () => {
assert.equal(
joinSoftWrappedRows("https://example.com/very/long/ ", "path"),
"https://example.com/very/long/path",
);
// Complete URL + padding + next word stays two words (not mid-token glue).
assert.equal(
joinSoftWrappedRows("See https://example.com ", "today"),
"See https://example.com today",
);
});
test("joinSoftWrappedRows preserves Windows paths and URL query delimiters", () => {
assert.equal(
joinSoftWrappedRows("C:\\Users\\alice\\ ", "file.txt"),
"C:\\Users\\alice\\file.txt",
);
assert.equal(
joinSoftWrappedRows("https://example.com/path ", "?q=netcatty"),
"https://example.com/path?q=netcatty",
);
assert.equal(
joinSoftWrappedRows("https://example.com/search? ", "q=netcatty"),
"https://example.com/search?q=netcatty",
);
});
test("joinSoftWrappedRows keeps sentence break after a URL", () => {
assert.equal(
joinSoftWrappedRows("See https://example.com. ", "Next sentence"),
"See https://example.com. Next sentence",
);
});
test("joinSoftWrappedRows keeps hyphenated words intact at the hyphen", () => {
assert.equal(joinSoftWrappedRows("state- ", "of-the-art"), "state-of-the-art");
// Complete flag + next word keeps a boundary.
assert.equal(joinSoftWrappedRows("Use --verbose ", "to inspect"), "Use --verbose to inspect");
});
test("preserves partial trailing spaces after wide characters using column ends", () => {
// "中 X" with 中 width 2 → columns: [中][ ][ ][ ][X] roughly.
// Select through the spaces after 中 but before X.
const term = makeTerm(
[{ text: "中 X", cellWidths: [2, 1, 1, 1] }],
{ start: { x: 0, y: 0 }, end: { x: 4, y: 0 } },
);
// end.x=4 is before content end column of X; keep selected spaces.
assert.equal(getNormalizedTerminalSelection(term), "中 ");
});
test("joins soft-wrapped rows and collapses prose padding to one space", () => {
const term = makeTerm(
[
{ text: "Pi: use /copy is the most " },
{ text: "reliable option ", isWrapped: true },
{ text: "next hard line " },
],
{ start: { x: 0, y: 0 }, end: { x: 28, y: 2 } },
);
assert.equal(
getNormalizedTerminalSelection(term),
"Pi: use /copy is the most reliable option\nnext hard line",
);
});
test("keeps a single trailing space as a word-boundary soft wrap", () => {
const term = makeTerm(
[
{ text: "hello " },
{ text: "world ", isWrapped: true },
],
{ start: { x: 0, y: 0 }, end: { x: 8, y: 1 } },
);
assert.equal(getNormalizedTerminalSelection(term), "hello world");
});
test("preserves hard line breaks between non-wrapped rows while trimming padding", () => {
const term = makeTerm(
[
{ text: "line one " },
{ text: "line two " },
{ text: "line three " },
],
{ start: { x: 0, y: 0 }, end: { x: 11, y: 2 } },
);
assert.equal(getNormalizedTerminalSelection(term), "line one\nline two\nline three");
});
test("preserves explicitly selected trailing spaces on a partial last row", () => {
// "abc def" — select columns 0..5 → "abc " must keep the spaces.
const term = makeTerm(
[{ text: "abc def" }],
{ start: { x: 0, y: 0 }, end: { x: 5, y: 0 } },
);
assert.equal(getNormalizedTerminalSelection(term), "abc ");
});
test("empty-cell trim from xterm still applies before written-space trim", () => {
const term = makeTerm(
[{ text: "hello", emptyCells: 10 }],
{ start: { x: 0, y: 0 }, end: { x: 15, y: 0 } },
);
assert.equal(getNormalizedTerminalSelection(term), "hello");
});
test("respects partial column selection on first and last rows", () => {
const term = makeTerm(
[
{ text: "xxhello worldyy" },
{ text: "continued here!", isWrapped: true },
],
{ start: { x: 2, y: 0 }, end: { x: 10, y: 1 } },
);
// Last-row end.x=10 selects "continued " (including the space); keep it.
assert.equal(getNormalizedTerminalSelection(term), "hello worldyycontinued ");
});
test("falls back to getSelection when position is unavailable", () => {
const term = makeTerm([{ text: "abc" }], null, { rawSelection: "fallback text" });
assert.equal(getNormalizedTerminalSelection(term), "fallback text");
});
test("getTerminalSelectionForClipboard respects normalize flag", () => {
const term = makeTerm(
[
{ text: "hello " },
{ text: "world ", isWrapped: true },
],
{ start: { x: 0, y: 0 }, end: { x: 8, y: 1 } },
{ rawSelection: "hello \nworld " },
);
assert.equal(getTerminalSelectionForClipboard(term, true), "hello world");
assert.equal(getTerminalSelectionForClipboard(term, false), "hello \nworld ");
});
test("returns empty string for empty range and normalizes inverted ranges", () => {
const empty = makeTerm([{ text: "abc" }], { start: { x: 1, y: 0 }, end: { x: 1, y: 0 } });
assert.equal(getNormalizedTerminalSelection(empty), "");
const inverted = makeTerm(
[
{ text: "alpha " },
{ text: "beta " },
],
{ start: { x: 6, y: 1 }, end: { x: 0, y: 0 } },
);
assert.equal(getNormalizedTerminalSelection(inverted), "alpha\nbeta");
});
test("handles multi-row soft wrap chains", () => {
const term = makeTerm(
[
{ text: "aaa" },
{ text: "bbb", isWrapped: true },
{ text: "ccc", isWrapped: true },
{ text: "ddd" },
],
{ start: { x: 0, y: 0 }, end: { x: 3, y: 3 } },
);
assert.equal(getNormalizedTerminalSelection(term), "aaabbbccc\nddd");
});
test("preserves rectangular column selection including right-edge spaces", () => {
const term = makeTerm(
[
{ text: "ab efghij" },
{ text: "01 56789x" },
{ text: "AB EFGHIJ" },
],
{ start: { x: 2, y: 0 }, end: { x: 5, y: 2 } },
{ columnSelect: true },
);
// Columns 2..5 include the intentional spaces.
assert.equal(getNormalizedTerminalSelection(term), " e\n 5\n E");
});
test("converts non-breaking spaces to regular spaces", () => {
const term = makeTerm(
[{ text: "hello\u00a0world " }],
{ start: { x: 0, y: 0 }, end: { x: 13, y: 0 } },
);
assert.equal(getNormalizedTerminalSelection(term), "hello world");
});
test("soft-wrapped CJK with padding joins without inserted spaces", () => {
const term = makeTerm(
[
{ text: "Pi: 用 /copy 最 " },
{ text: "稳 ", isWrapped: true },
],
{ start: { x: 0, y: 0 }, end: { x: 18, y: 1 } },
);
assert.equal(getNormalizedTerminalSelection(term), "Pi: 用 /copy 最稳");
});
test("keeps a wrap-boundary space that canonical translateToString trimEnd would drop", () => {
const term = makeTerm(
[
{ text: "|hbase ", canonicalTrimEnd: true },
{ text: "shell", isWrapped: true, canonicalTrimEnd: true },
],
{ start: { x: 0, y: 0 }, end: { x: 5, y: 1 } },
);
assert.equal(getNormalizedTerminalSelection(term), "|hbase shell");
});
test("real xterm keeps a wrap-boundary space after an untrimmed string-cache prime", async () => {
const xterm = new XTerm({ cols: 20, rows: 8, scrollback: 20, allowProposedApi: true });
try {
await new Promise<void>((resolve) => {
xterm.write("12345678901234hbase shell\r\n", resolve);
});
// Prompt detector / autocomplete / keyword highlight all call this first.
xterm.buffer.active.getLine(0)?.translateToString(false);
const wrapped: SelectionTerminal = {
getSelection: () => xterm.getSelection(),
getSelectionPosition: () => ({ start: { x: 0, y: 0 }, end: { x: 5, y: 1 } }),
buffer: xterm.buffer,
};
assert.equal(
xterm.buffer.active.getLine(0)?.translateToString(true),
"12345678901234hbase",
);
assert.equal(getNormalizedTerminalSelection(wrapped), "12345678901234hbase shell");
} finally {
xterm.dispose();
}
});