mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-11-07 16:41:30 +00:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f42e870e74 | ||
|
|
24c2e6beff | ||
|
|
c59ea270e2 | ||
|
|
9b29c115df |
No files matched your search
@@ -2776,6 +2776,15 @@ export const createXTermRuntime = (ctx: CreateXTermRuntimeContext): XTermRuntime
|
||||
};
|
||||
return true;
|
||||
}
|
||||
// xterm leaves Shift+Insert to the browser's native paste handler. Fan out
|
||||
// the clipboard data from onData instead of encoding an Insert key for
|
||||
// peers or treating the paste as that key's pending legacy data (#4142).
|
||||
if (
|
||||
e.key === "Insert" && e.shiftKey
|
||||
&& !e.ctrlKey && !e.altKey && !e.metaKey
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (!shouldDeferKittyKeyEvent(normalizedKittyEvent)) {
|
||||
const identity = kittyKeyIdentity(e);
|
||||
if (
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import test from "node:test";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { buildSync } from "esbuild";
|
||||
import { JSDOM } from "jsdom";
|
||||
import ts from "typescript";
|
||||
|
||||
test("native Shift+Insert broadcasts clipboard bytes once without an Insert key", async () => {
|
||||
const root = fileURLToPath(new URL("../../..", import.meta.url));
|
||||
const layer = readFileSync(new URL("../../TerminalLayer.tsx", import.meta.url), "utf8");
|
||||
const start = layer.indexOf(" const handleBroadcastInput = useCallback(");
|
||||
const end = layer.indexOf(" const handleCommandSubmitted =", start);
|
||||
assert.ok(start >= 0 && end > start);
|
||||
const dispatcher = ts.transpileModule(layer.slice(start, end), {
|
||||
compilerOptions: { target: ts.ScriptTarget.ES2022 },
|
||||
}).outputText;
|
||||
const bundle = buildSync({
|
||||
stdin: {
|
||||
contents: `
|
||||
export { createXTermRuntime } from "./components/terminal/runtime/createXTermRuntime";
|
||||
export { DEFAULT_TERMINAL_SETTINGS } from "./domain/models/terminal";
|
||||
export { resolveTerminalBroadcastTargetIds } from "./domain/terminalBroadcast";
|
||||
export { dispatchKittyKeyboardBroadcastInput } from "./components/terminal/runtime/kittyKeyboardBroadcast";
|
||||
export * from "./components/terminal/runtime/terminalPacedBroadcast";
|
||||
`,
|
||||
loader: "ts", resolveDir: root,
|
||||
},
|
||||
bundle: true, format: "iife", globalName: "runtimeTest", platform: "browser", write: false,
|
||||
alias: {
|
||||
"node:diagnostics_channel": path.join(root, "infrastructure/shims/nodeDiagnosticsChannel.ts"),
|
||||
"diagnostics_channel": path.join(root, "infrastructure/shims/nodeDiagnosticsChannel.ts"),
|
||||
},
|
||||
define: { "import.meta.env.DEV": "false", "import.meta.env.PROD": "true", "import.meta": "{}" },
|
||||
}).outputFiles[0].text;
|
||||
const dom = new JSDOM("<body/>", { url: "http://localhost", runScripts: "outside-only", pretendToBeVisual: true });
|
||||
Object.assign(dom.window, { structuredClone });
|
||||
try {
|
||||
dom.window.eval(`
|
||||
Object.defineProperty(navigator, 'platform', {value:'Win32'});
|
||||
window.matchMedia = () => ({matches:false,addListener(){},removeListener(){},addEventListener(){},removeEventListener(){}});
|
||||
window.ResizeObserver = window.IntersectionObserver = class {observe(){} unobserve(){} disconnect(){}};
|
||||
HTMLCanvasElement.prototype.getContext = function(type) {
|
||||
if (type !== '2d') return null;
|
||||
return new Proxy({canvas:this,measureText:()=>({width:8,fontBoundingBoxAscent:12,fontBoundingBoxDescent:3}),
|
||||
getImageData:()=>({data:new Uint8ClampedArray(4)}),createLinearGradient:()=>({addColorStop(){}})},
|
||||
{get:(target,key)=>key in target ? target[key] : ()=>{}});
|
||||
};
|
||||
`);
|
||||
dom.window.eval(bundle);
|
||||
const results = await dom.window.eval(`(async () => {
|
||||
const {createXTermRuntime, DEFAULT_TERMINAL_SETTINGS, resolveTerminalBroadcastTargetIds,
|
||||
dispatchKittyKeyboardBroadcastInput, markTerminalBroadcastUserInput,
|
||||
captureTerminalBroadcastInput, isTerminalBroadcastInputCurrent} = runtimeTest;
|
||||
const ref = current => ({current});
|
||||
const useCallback = fn => fn;
|
||||
const sessionsRef = ref([]), isGlobalBroadcastEnabled = false;
|
||||
const broadcastPasswordBypassRef = ref(false);
|
||||
const canUseDirectSessionWriteFallback = () => true;
|
||||
const isTerminalSensitiveInputActive = () => false;
|
||||
const wires = new Map();
|
||||
const terminalBackend = {
|
||||
writeToSession:(id,data)=>wires.get(id).push(data), resizeSession(){}, openExternalAvailable:false,
|
||||
};
|
||||
${dispatcher}
|
||||
const make = (id, kittyKeyboardProtocolEnabled) => {
|
||||
const container = document.createElement('div'); document.body.appendChild(container);
|
||||
wires.set(id, []);
|
||||
const host = {id,label:id,hostname:'localhost',protocol:'ssh',sshInputCompatibility:false};
|
||||
return createXTermRuntime({
|
||||
container, host, getHost:()=>host, sessionId:id, sessionRef:ref(id), statusRef:ref('connected'),
|
||||
fontFamilyId:'jetbrains-mono',resolvedFontFamily:'monospace',fontSize:14,
|
||||
terminalTheme:{colors:{background:'#111111',foreground:'#eeeeee',cursor:'#ffffff',selection:'#444444'}},
|
||||
terminalSettingsRef:ref({...DEFAULT_TERMINAL_SETTINGS,rendererType:'dom',fontLigatures:false,cursorBlink:false}),
|
||||
terminalBackend,hotkeySchemeRef:ref('disabled'),keyBindingsRef:ref([]),onHotkeyActionRef:ref(undefined),
|
||||
isBroadcastEnabledRef:ref(true),onBroadcastInputRef:ref(handleBroadcastInput),
|
||||
commandBufferRef:ref(''),requestSearchFocus(){},kittyKeyboardProtocolEnabled,
|
||||
});
|
||||
};
|
||||
const results = [];
|
||||
for (const bracketed of [false,true]) {
|
||||
for (const delayed of [true,false]) {
|
||||
for (const peerKitty of [false,true]) {
|
||||
const source = make('source', false), peer = make('peer', peerKitty);
|
||||
try {
|
||||
sessionsRef.current = ['source','peer'].map(id=>({id,protocol:'ssh',workspaceId:'workspace'}));
|
||||
if (peerKitty) await new Promise(resolve=>peer.term.write('\\x1b[>8u',resolve));
|
||||
if (bracketed) await new Promise(resolve=>source.term.write('\\x1b[?2004h',resolve));
|
||||
const key = new KeyboardEvent('keydown', {
|
||||
bubbles:true,cancelable:true,key:'Insert',code:'Insert',keyCode:45,shiftKey:true,
|
||||
});
|
||||
source.term.textarea.dispatchEvent(key);
|
||||
const beforePaste = [...wires.get('peer')];
|
||||
if (delayed) await new Promise(resolve=>setTimeout(resolve,10));
|
||||
// Supply the native clipboard event which Chromium emits for this chord.
|
||||
const paste = new Event('paste', {bubbles:true,cancelable:true});
|
||||
Object.defineProperty(paste,'clipboardData',{value:{getData:()=> 'clipboard'}});
|
||||
source.term.textarea.dispatchEvent(paste);
|
||||
source.term.textarea.dispatchEvent(new KeyboardEvent('keyup', {
|
||||
bubbles:true,key:'Insert',code:'Insert',keyCode:45,shiftKey:true,
|
||||
}));
|
||||
results.push({bracketed,delayed,peerKitty,beforePaste,
|
||||
prevented:key.defaultPrevented,source:[...wires.get('source')],peer:[...wires.get('peer')]});
|
||||
// Ordinary Insert still uses the physical-key broadcast path.
|
||||
if (!peerKitty) {
|
||||
wires.get('source').length=0; wires.get('peer').length=0;
|
||||
source.term.textarea.dispatchEvent(new KeyboardEvent('keydown', {
|
||||
bubbles:true,cancelable:true,key:'Insert',code:'Insert',keyCode:45,
|
||||
}));
|
||||
results.push({plainInsert:true,source:[...wires.get('source')],peer:[...wires.get('peer')]});
|
||||
}
|
||||
} finally { source.dispose(); peer.dispose(); }
|
||||
}
|
||||
}
|
||||
}
|
||||
return JSON.stringify(results);
|
||||
})()`);
|
||||
for (const result of JSON.parse(results)) {
|
||||
if (result.plainInsert) {
|
||||
assert.deepEqual(result.source, ["\x1b[2~"]);
|
||||
assert.deepEqual(result.peer, ["\x1b[2~"]);
|
||||
continue;
|
||||
}
|
||||
const label = JSON.stringify(result);
|
||||
assert.deepEqual(result.beforePaste, [], label);
|
||||
assert.equal(result.prevented, false, "native paste must remain enabled: " + label);
|
||||
const expected = result.bracketed ? "\x1b[200~clipboard\x1b[201~" : "clipboard";
|
||||
assert.deepEqual(result.source, [expected], label);
|
||||
assert.deepEqual(result.peer, [expected], label);
|
||||
}
|
||||
} finally {
|
||||
dom.window.close();
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user