[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,163 @@
/**
* Terminal cell-column width for autocomplete / ghost positioning.
*
* When an xterm instance is available, prefer its active Unicode provider
* (`15-graphemes` via UnicodeGraphemesAddon) so emoji / VS-16 clusters match
* the cursor advance. Fall back to a small East-Asian-Width-style classifier
* for unit fakes that lack `_core.unicodeService`.
*/
import type { Terminal as XTerm } from "@xterm/xterm";
type UnicodeServiceLike = {
getStringCellWidth?: (s: string) => number;
};
type TermWithUnicodeService = {
_core?: {
unicodeService?: UnicodeServiceLike;
};
};
const unicodeMarkPattern = /\p{Mark}/u;
function codePointCellWidth(cp: number): number {
// Zero-width joiners / format / variation selectors / marks — xterm
// folds these into the surrounding grapheme (wcwidth 0 or shouldJoin).
if (
cp === 0x00ad ||
cp === 0x200d || // ZWJ
(cp >= 0x200b && cp <= 0x200f) || // ZWSP..RLM
(cp >= 0x202a && cp <= 0x202e) || // bidi overrides
(cp >= 0x2060 && cp <= 0x206f) || // word joiner, invisible ops
(cp >= 0xfe00 && cp <= 0xfe0f) || // Variation Selectors
cp === 0xfeff ||
(cp >= 0x1f3fb && cp <= 0x1f3ff) || // Emoji skin-tone modifiers
(cp >= 0xe0100 && cp <= 0xe01ef) || // Variation Selectors Supplement
unicodeMarkPattern.test(String.fromCodePoint(cp))
) {
return 0;
}
if (
(cp >= 0x1100 && cp <= 0x115f) || // Hangul Jamo
(cp >= 0x2e80 && cp <= 0x303e) || // CJK Radicals, Kangxi
(cp >= 0x3041 && cp <= 0x33ff) || // Hiragana, Katakana, CJK Compat
(cp >= 0x3400 && cp <= 0x4dbf) || // CJK Extension A
(cp >= 0x4e00 && cp <= 0x9fff) || // CJK Unified Ideographs
(cp >= 0xa000 && cp <= 0xa4cf) || // Yi
(cp >= 0xac00 && cp <= 0xd7a3) || // Hangul Syllables
(cp >= 0xf900 && cp <= 0xfaff) || // CJK Compat Ideographs
(cp >= 0xfe30 && cp <= 0xfe4f) || // CJK Compat Forms
(cp >= 0xff00 && cp <= 0xff60) || // Fullwidth forms
(cp >= 0xffe0 && cp <= 0xffe6) || // Fullwidth signs
(cp >= 0x1f300 && cp <= 0x1faff) || // Emoji blocks
(cp >= 0x20000 && cp <= 0x3fffd) // CJK Extension B-F, G
) {
return 2;
}
return 1;
}
function graphemeCellWidth(grapheme: string): number {
let max = 0;
for (const ch of grapheme) {
const w = codePointCellWidth(ch.codePointAt(0) ?? 0);
if (w > max) max = w;
}
return max;
}
const graphemeSegmenter =
typeof Intl !== "undefined" && "Segmenter" in Intl
? new Intl.Segmenter(undefined, { granularity: "grapheme" })
: null;
function fallbackStringCellWidth(s: string): number {
if (graphemeSegmenter) {
let w = 0;
for (const { segment } of graphemeSegmenter.segment(s)) {
w += graphemeCellWidth(segment);
}
return w;
}
// Fallback without Segmenter: sum code-point widths (ZWJ/marks already 0).
let w = 0;
for (const ch of s) {
w += codePointCellWidth(ch.codePointAt(0) ?? 0);
}
return w;
}
/** Terminal cell columns occupied by `s` (wide glyphs / grapheme clusters). */
export function stringCellWidth(
s: string,
term?: XTerm | TermWithUnicodeService | null,
): number {
if (!s) return 0;
const unicodeService = (term as TermWithUnicodeService | null | undefined)
?._core?.unicodeService;
const getWidth = unicodeService?.getStringCellWidth;
if (typeof getWidth === "function") {
return getWidth.call(unicodeService, s);
}
return fallbackStringCellWidth(s);
}
/**
* Slice a terminal line string by cell columns (xterm `cursorX` units).
*
* `translateToString()` returns characters, but `buffer.cursorX` is a cell
* column. Mixing them with `String#substring(cursorX)` pulls padding spaces
* into user input whenever the prompt contains wide glyphs (CJK paths in
* Windows CMD / PowerShell), which breaks autocomplete matching (#2813).
*/
export function sliceStringByCellColumns(
text: string,
startCell: number,
endCell?: number,
term?: XTerm | TermWithUnicodeService | null,
): string {
if (!text) return "";
const start = Math.max(0, startCell);
const end = endCell === undefined ? Number.POSITIVE_INFINITY : Math.max(start, endCell);
if (end === 0) return "";
let cell = 0;
let startIndex = 0;
let endIndex = text.length;
let sawStart = false;
const advance = (segment: string, index: number, segmentLength: number): boolean => {
const width = stringCellWidth(segment, term);
const nextCell = cell + width;
if (!sawStart && nextCell > start) {
startIndex = index;
sawStart = true;
}
if (nextCell >= end) {
endIndex = nextCell === end ? index + segmentLength : index;
if (!sawStart) {
startIndex = index;
sawStart = true;
}
return true;
}
cell = nextCell;
return false;
};
if (graphemeSegmenter) {
for (const { segment, index } of graphemeSegmenter.segment(text)) {
if (advance(segment, index, segment.length)) break;
}
} else {
let index = 0;
for (const ch of text) {
if (advance(ch, index, ch.length)) break;
index += ch.length;
}
}
if (!sawStart) return "";
return text.slice(startIndex, endIndex);
}