mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-11-06 14:30:13 +00:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
98c4efb6ff | ||
|
|
940bddf5b2 | ||
|
|
7ad3263fa4 | ||
|
|
c0d66cc8a6 | ||
|
|
28b9db7e81 | ||
|
|
ff4a48916f |
No files matched your search
@@ -66,6 +66,7 @@ const createEraseHandlerHarness = (
|
||||
};
|
||||
const observedScrollRegions: Array<[number, number]> = [];
|
||||
const term = {
|
||||
modes: { synchronizedOutputMode: options.inDec2026SyncBlock ?? false },
|
||||
rows: 5,
|
||||
options: {
|
||||
scrollOnEraseInDisplay: false,
|
||||
@@ -118,7 +119,6 @@ const createEraseHandlerHarness = (
|
||||
};
|
||||
const disposable = installEraseInDisplayHandlers(term as never, {
|
||||
getClearWipesScrollback: () => options.clearWipesScrollback ?? false,
|
||||
isInDec2026SyncBlock: () => options.inDec2026SyncBlock ?? false,
|
||||
scheduleMicrotask: (callback) => {
|
||||
microtasks.push(callback);
|
||||
},
|
||||
@@ -321,7 +321,6 @@ test("erase-below wipe preserves later output from the same write batch", async
|
||||
const term = new Terminal({ cols: 20, rows: 5, scrollback: 100 });
|
||||
const disposable = installEraseInDisplayHandlers(term as never, {
|
||||
getClearWipesScrollback: () => true,
|
||||
isInDec2026SyncBlock: () => false,
|
||||
});
|
||||
|
||||
await writeTerminal(term, "old1\r\nold2\r\nold3\r\nold4\r\nold5\r\nold6\r\nold7\r\nold8");
|
||||
@@ -524,3 +523,96 @@ test("appendEraseScrollback still wipes when delayed clear is outside a sync blo
|
||||
"\x1b[H\x1b[2J\x1b[3Jframe",
|
||||
);
|
||||
});
|
||||
|
||||
const getScrollbackLines = (term: InstanceType<typeof Terminal>): string[] =>
|
||||
Array.from({ length: term.buffer.active.baseY }, (_, row) =>
|
||||
term.buffer.active.getLine(row)?.translateToString(true) ?? ""
|
||||
);
|
||||
|
||||
test("native synchronized-output mode ignores unrelated private modes", async () => {
|
||||
const term = new Terminal({ cols: 20, rows: 5, scrollback: 100 });
|
||||
|
||||
await writeTerminal(term, "\x1b[?2004h");
|
||||
assert.equal(term.modes.synchronizedOutputMode, false, "bracketed paste 2004 must not open a sync block");
|
||||
|
||||
await writeTerminal(term, "\x1b[?25h\x1b[?1049h");
|
||||
assert.equal(term.modes.synchronizedOutputMode, false, "cursor/alt-screen modes must not open a sync block");
|
||||
|
||||
await writeTerminal(term, "\x1b[?25:2026h");
|
||||
assert.equal(term.modes.synchronizedOutputMode, false, "subparameters do not enable sync mode");
|
||||
|
||||
await writeTerminal(term, "\x1b[?25;2026h");
|
||||
assert.equal(term.modes.synchronizedOutputMode, true, "combined primary modes enable sync mode");
|
||||
|
||||
await writeTerminal(term, "\x1b[?2004l");
|
||||
assert.equal(term.modes.synchronizedOutputMode, true, "closing 2004 must not close the 2026 block");
|
||||
|
||||
await writeTerminal(term, "\x1b[?2026l");
|
||||
assert.equal(term.modes.synchronizedOutputMode, false);
|
||||
|
||||
term.dispose();
|
||||
});
|
||||
|
||||
test("Ctrl+L clear survives a bash prompt (2004 open, 2026 closed)", async () => {
|
||||
const term = new Terminal({ cols: 20, rows: 5, scrollback: 100 });
|
||||
const erase = installEraseInDisplayHandlers(term as never, {
|
||||
getClearWipesScrollback: () => false,
|
||||
});
|
||||
|
||||
// Exactly two rows already in scrollback, then a full screen of visible rows.
|
||||
await writeTerminal(term, "old1\r\nold2\r\nvis1\r\nvis2\r\nvis3\r\nvis4\r\nvis5");
|
||||
assert.equal(term.buffer.active.baseY, 2);
|
||||
|
||||
// bash 5.1 at a prompt holds bracketed paste open; Ctrl+L only sends CSI 2 J.
|
||||
await writeTerminal(term, "\x1b[?2004h");
|
||||
await writeTerminal(term, "\x1b[H\x1b[2Jnew1");
|
||||
|
||||
const scrollback = getScrollbackLines(term);
|
||||
assert.equal(scrollback.some((line) => line.startsWith("vis")), true, "visible rows preserved in scrollback");
|
||||
assert.equal(scrollback.some((line) => line.startsWith("old")), true, "earlier history not trimmed");
|
||||
|
||||
erase.dispose();
|
||||
term.dispose();
|
||||
});
|
||||
|
||||
test("clears inside a DEC 2026 sync block still erase in place", async () => {
|
||||
const term = new Terminal({ cols: 20, rows: 5, scrollback: 100 });
|
||||
const erase = installEraseInDisplayHandlers(term as never, {
|
||||
getClearWipesScrollback: () => false,
|
||||
});
|
||||
|
||||
await writeTerminal(term, "old1\r\nold2\r\nvis1\r\nvis2\r\nvis3\r\nvis4\r\nvis5");
|
||||
assert.equal(term.buffer.active.baseY, 2);
|
||||
await writeTerminal(term, "\x1b[?2026h\x1b[H\x1b[2Jnew1\x1b[?2026l");
|
||||
|
||||
const scrollback = getScrollbackLines(term);
|
||||
assert.equal(scrollback.some((line) => line.startsWith("vis")), false, "sync-block erase stays in place");
|
||||
assert.equal(scrollback.some((line) => line.startsWith("old")), true, "only pre-existing scrollback remains");
|
||||
|
||||
erase.dispose();
|
||||
term.dispose();
|
||||
});
|
||||
|
||||
for (const [label, sequence, preservesViewport] of [
|
||||
["combined primary modes", "\x1b[?25;2026h", false],
|
||||
["subparameter only", "\x1b[?25:2026h", true],
|
||||
["explicit reset", "\x1b[?2026h\x1b[?2026l", true],
|
||||
] as const) {
|
||||
test(`full erase uses native synchronized-output state for ${label}`, async () => {
|
||||
const term = new Terminal({ cols: 20, rows: 5, scrollback: 100 });
|
||||
const erase = installEraseInDisplayHandlers(term, {
|
||||
getClearWipesScrollback: () => false,
|
||||
});
|
||||
try {
|
||||
await writeTerminal(term, "old1\r\nold2\r\nvis1\r\nvis2\r\nvis3\r\nvis4\r\nvis5");
|
||||
// The mode and erase arrive in separate PTY chunks, as with TUI redraws.
|
||||
await writeTerminal(term, sequence);
|
||||
await writeTerminal(term, "\x1b[H\x1b[2Jnew1");
|
||||
assert.equal(getScrollbackLines(term).some((line) => line.startsWith("vis")), preservesViewport);
|
||||
assert.equal(getScrollbackLines(term).some((line) => line.startsWith("old")), true);
|
||||
} finally {
|
||||
erase.dispose();
|
||||
term.dispose();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -47,7 +47,6 @@ type AppendEraseScrollbackOptions = {
|
||||
|
||||
type EraseInDisplayHandlerOptions = {
|
||||
getClearWipesScrollback: () => boolean;
|
||||
isInDec2026SyncBlock: () => boolean;
|
||||
scheduleMicrotask?: (callback: () => void) => void;
|
||||
};
|
||||
|
||||
@@ -264,7 +263,6 @@ export const installEraseInDisplayHandlers = (
|
||||
term: EraseInDisplayTerminal,
|
||||
{
|
||||
getClearWipesScrollback,
|
||||
isInDec2026SyncBlock,
|
||||
scheduleMicrotask = queueMicrotask,
|
||||
}: EraseInDisplayHandlerOptions,
|
||||
): IDisposable => {
|
||||
@@ -279,7 +277,8 @@ export const installEraseInDisplayHandlers = (
|
||||
|
||||
const eraseDisposable = term.parser.registerCsiHandler({ final: "J" }, (params) => {
|
||||
const wipeAllowed = getClearWipesScrollback();
|
||||
const inDec2026SyncBlock = isInDec2026SyncBlock();
|
||||
// Use xterm's parsed mode, including its native safety timeout.
|
||||
const inDec2026SyncBlock = term.modes.synchronizedOutputMode;
|
||||
// Scope xterm's native preservation to shell clears, not TUI redraws.
|
||||
if (isEraseViewportSequence(params)) {
|
||||
const useNativeScrollPreservation = shouldScrollOnEraseInDisplay(
|
||||
|
||||
@@ -3170,28 +3170,8 @@ export const createXTermRuntime = (ctx: CreateXTermRuntimeContext): XTermRuntime
|
||||
// OSC 7 format: \x1b]7;file://hostname/path\x07 or \x1b]7;file://hostname/path\x1b\\
|
||||
let currentCwd: string | undefined = undefined;
|
||||
|
||||
// Track DEC 2026 synchronized-output blocks so CSI 2 J can erase in place for
|
||||
// Codex/Claude Code TUIs instead of pushing visible rows into scrollback.
|
||||
let inDec2026SyncBlock = false;
|
||||
|
||||
const dec2026SyncStartDisposable = term.parser.registerCsiHandler(
|
||||
{ prefix: "?", final: "h", params: [2026] },
|
||||
() => {
|
||||
inDec2026SyncBlock = true;
|
||||
return false;
|
||||
},
|
||||
);
|
||||
const dec2026SyncEndDisposable = term.parser.registerCsiHandler(
|
||||
{ prefix: "?", final: "l", params: [2026] },
|
||||
() => {
|
||||
inDec2026SyncBlock = false;
|
||||
return false;
|
||||
},
|
||||
);
|
||||
|
||||
const eraseScrollbackDisposable = installEraseInDisplayHandlers(term, {
|
||||
getClearWipesScrollback: () => ctx.terminalSettingsRef.current?.clearWipesScrollback ?? true,
|
||||
isInDec2026SyncBlock: () => inDec2026SyncBlock,
|
||||
});
|
||||
|
||||
const markCursorPositionReportRequest = (params: readonly (number | number[])[]): boolean => {
|
||||
@@ -3500,8 +3480,6 @@ export const createXTermRuntime = (ctx: CreateXTermRuntimeContext): XTermRuntime
|
||||
pluginLinkProviderHost?.dispose();
|
||||
pluginProviderHost?.dispose();
|
||||
eraseScrollbackDisposable.dispose();
|
||||
dec2026SyncStartDisposable.dispose();
|
||||
dec2026SyncEndDisposable.dispose();
|
||||
for (const disposable of cursorPositionReportRequestDisposables) {
|
||||
disposable.dispose();
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user