mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-11-08 00:50:06 +00:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0f0c0d93c | ||
|
|
1d02117022 | ||
|
|
7ef175ce08 | ||
|
|
dbf92ee701 | ||
|
|
f6f20009e9 |
No files matched your search
@@ -4219,6 +4219,7 @@ const TerminalComponent: React.FC<TerminalProps> = ({
|
||||
|
||||
xTermRuntimeContextRef.current = {
|
||||
host,
|
||||
localShellType: shellType,
|
||||
fontFamilyId,
|
||||
resolvedFontFamily,
|
||||
fontSize: effectiveFontSize,
|
||||
|
||||
@@ -165,6 +165,7 @@ import {
|
||||
type TerminalOutputHistoryPreview,
|
||||
} from "./terminalOutputHistory";
|
||||
import { shouldPassThroughCopyShortcut } from "./terminalCopyShortcut";
|
||||
import { shouldPastePlainCtrlV } from "./terminalPasteChord";
|
||||
import {
|
||||
isMacCommandPeriodInterruptChord,
|
||||
shouldUseUrgentTerminalInterrupt,
|
||||
@@ -302,6 +303,7 @@ export const resetKittyKeyboardModeStateForSession = (
|
||||
export type CreateXTermRuntimeContext = {
|
||||
container: HTMLDivElement;
|
||||
host: Host;
|
||||
localShellType?: TerminalSession["shellType"];
|
||||
fontFamilyId: string;
|
||||
resolvedFontFamily: string;
|
||||
fontSize: number;
|
||||
@@ -2419,6 +2421,37 @@ export const createXTermRuntime = (ctx: CreateXTermRuntimeContext): XTermRuntime
|
||||
}
|
||||
}
|
||||
|
||||
// Clipboard-based dictation tools can deliver a transcript by placing it
|
||||
// on the clipboard and simulating plain Ctrl+V. The #3468 reporter
|
||||
// confirmed this path for Wispr Flow by remapping Paste to Ctrl+V.
|
||||
// In xterm's legacy key path, plain Ctrl+V can reach the remote as \x16
|
||||
// (readline quoted-insert) instead of a paste (#3468). Route the chord
|
||||
// through the shared paste pipeline like the Ctrl+Shift+V binding: text
|
||||
// pastes, and a local image-only clipboard forwards raw Ctrl+V to nested
|
||||
// TUIs. Kitty and native Windows shells in Win32 input mode retain their
|
||||
// own encoding of the chord.
|
||||
// Gate on the actual OS: on other platforms plain Ctrl+V is a live
|
||||
// terminal key (readline quoted-insert, Vim visual-block), so it must
|
||||
// keep forwarding as \x16 regardless of the configured hotkey scheme.
|
||||
if (shouldPastePlainCtrlV(e, {
|
||||
platform,
|
||||
connected: Boolean(ctx.sessionRef.current) && ctx.statusRef.current === "connected",
|
||||
kittySequenceForKeyDown,
|
||||
win32InputMode: term.modes.win32InputMode,
|
||||
localShellType: ctx.localShellType,
|
||||
})) {
|
||||
const id = ctx.sessionRef.current;
|
||||
if (id) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
// Always share the context-menu paste path so local image-only
|
||||
// clipboard can forward Ctrl+V, and remote auto-upload stays
|
||||
// gated inside handleTerminalClipboardPaste.
|
||||
void ctx.terminalContextActionsRef?.current?.onPaste?.();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Sogou/macOS CJK punctuation: keydown still reports ASCII "," while the
|
||||
// full-width "," arrives on insertText. Returning false (without
|
||||
// preventDefault) stops xterm/Kitty from sending the half-width key so the
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { isPlainCtrlVPasteChord, shouldPastePlainCtrlV } from "./terminalPasteChord.ts";
|
||||
|
||||
const keyboardEvent = (
|
||||
key: string,
|
||||
code: string,
|
||||
modifiers: Partial<KeyboardEvent> = {},
|
||||
): KeyboardEvent => ({
|
||||
key,
|
||||
code,
|
||||
ctrlKey: false,
|
||||
shiftKey: false,
|
||||
altKey: false,
|
||||
metaKey: false,
|
||||
...modifiers,
|
||||
}) as KeyboardEvent;
|
||||
|
||||
test("plain Ctrl+V is the dictation paste chord", () => {
|
||||
assert.equal(isPlainCtrlVPasteChord(keyboardEvent("v", "KeyV", { ctrlKey: true })), true);
|
||||
assert.equal(isPlainCtrlVPasteChord(keyboardEvent("V", "KeyV", { ctrlKey: true })), true);
|
||||
});
|
||||
|
||||
test("plain Ctrl+V follows the physical V key on non-Latin layouts", () => {
|
||||
const event = keyboardEvent("м", "KeyV", { ctrlKey: true });
|
||||
|
||||
assert.equal(isPlainCtrlVPasteChord(event), true);
|
||||
});
|
||||
|
||||
test("Ctrl+V with extra modifiers or other keys is not the paste chord", () => {
|
||||
assert.equal(
|
||||
isPlainCtrlVPasteChord(keyboardEvent("v", "KeyV", { ctrlKey: true, shiftKey: true })),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
isPlainCtrlVPasteChord(keyboardEvent("v", "KeyV", { ctrlKey: true, altKey: true })),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
isPlainCtrlVPasteChord(keyboardEvent("v", "KeyV", { ctrlKey: true, metaKey: true })),
|
||||
false,
|
||||
);
|
||||
assert.equal(isPlainCtrlVPasteChord(keyboardEvent("c", "KeyC", { ctrlKey: true })), false);
|
||||
assert.equal(isPlainCtrlVPasteChord(keyboardEvent("v", "KeyV")), false);
|
||||
assert.equal(isPlainCtrlVPasteChord(keyboardEvent("v", "KeyV", { metaKey: true })), false);
|
||||
});
|
||||
|
||||
test("Windows legacy input pastes, including SSH sessions", () => {
|
||||
assert.equal(shouldPastePlainCtrlV(keyboardEvent("v", "KeyV", { ctrlKey: true }), {
|
||||
platform: "win32",
|
||||
connected: true,
|
||||
kittySequenceForKeyDown: null,
|
||||
win32InputMode: false,
|
||||
}), true);
|
||||
});
|
||||
|
||||
test("ConPTY Unix shells route Ctrl+V to paste while native Windows shells retain their key handling", () => {
|
||||
const event = keyboardEvent("v", "KeyV", { ctrlKey: true });
|
||||
const context = {
|
||||
platform: "win32" as const,
|
||||
connected: true,
|
||||
kittySequenceForKeyDown: null,
|
||||
win32InputMode: true,
|
||||
};
|
||||
for (const localShellType of ["posix", "fish"] as const) {
|
||||
assert.equal(shouldPastePlainCtrlV(event, { ...context, localShellType }), true, localShellType);
|
||||
}
|
||||
for (const localShellType of ["cmd", "powershell", "unknown"] as const) {
|
||||
assert.equal(shouldPastePlainCtrlV(event, { ...context, localShellType }), false, localShellType);
|
||||
}
|
||||
assert.equal(shouldPastePlainCtrlV(event, context), false);
|
||||
});
|
||||
|
||||
test("other platforms, negotiated Kitty keys, composition and disconnected sessions keep their key path", () => {
|
||||
const event = keyboardEvent("v", "KeyV", { ctrlKey: true });
|
||||
const context = {
|
||||
platform: "win32" as const,
|
||||
connected: true,
|
||||
kittySequenceForKeyDown: null,
|
||||
win32InputMode: false,
|
||||
};
|
||||
assert.equal(shouldPastePlainCtrlV(event, { ...context, platform: "linux" }), false);
|
||||
assert.equal(shouldPastePlainCtrlV(event, { ...context, platform: "darwin" }), false);
|
||||
assert.equal(shouldPastePlainCtrlV(event, { ...context, kittySequenceForKeyDown: "sequence" }), false);
|
||||
assert.equal(shouldPastePlainCtrlV(event, { ...context, connected: false }), false);
|
||||
assert.equal(shouldPastePlainCtrlV(keyboardEvent("v", "KeyV", {
|
||||
ctrlKey: true,
|
||||
isComposing: true,
|
||||
}), context), false);
|
||||
assert.equal(shouldPastePlainCtrlV(keyboardEvent("Process", "KeyV", {
|
||||
ctrlKey: true,
|
||||
keyCode: 229,
|
||||
}), context), false);
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
type PasteChordKeyEvent = Pick<
|
||||
KeyboardEvent,
|
||||
"key" | "code" | "ctrlKey" | "shiftKey" | "altKey" | "metaKey" | "isComposing" | "keyCode"
|
||||
>;
|
||||
|
||||
type PasteChordContext = {
|
||||
platform: "darwin" | "linux" | "win32";
|
||||
connected: boolean;
|
||||
kittySequenceForKeyDown: string | null;
|
||||
win32InputMode: boolean;
|
||||
localShellType?: "posix" | "fish" | "powershell" | "cmd" | "unknown";
|
||||
};
|
||||
|
||||
function isPhysicalVKey(e: PasteChordKeyEvent): boolean {
|
||||
return e.key.toLowerCase() === "v" || e.code === "KeyV";
|
||||
}
|
||||
|
||||
/**
|
||||
* Unmodified Ctrl+V — the system paste chord on Windows.
|
||||
*
|
||||
* Clipboard-based dictation tools can deliver their transcript by placing it
|
||||
* on the clipboard and simulating the system paste shortcut. The #3468
|
||||
* reporter confirmed this path for Wispr Flow by remapping Paste to Ctrl+V.
|
||||
* In xterm's legacy key path, plain Ctrl+V can reach the remote as \x16
|
||||
* (readline quoted-insert) instead of a paste (#3468).
|
||||
*
|
||||
* Also matches non-Latin layouts via the physical KeyV code, mirroring
|
||||
* isPlainCtrlCInterruptChord.
|
||||
*/
|
||||
export function isPlainCtrlVPasteChord(e: PasteChordKeyEvent): boolean {
|
||||
return e.ctrlKey
|
||||
&& !e.shiftKey
|
||||
&& !e.altKey
|
||||
&& !e.metaKey
|
||||
&& isPhysicalVKey(e);
|
||||
}
|
||||
|
||||
export function shouldPastePlainCtrlV(e: PasteChordKeyEvent, context: PasteChordContext): boolean {
|
||||
return context.platform === "win32"
|
||||
&& context.connected
|
||||
&& !context.kittySequenceForKeyDown
|
||||
&& (!context.win32InputMode || context.localShellType === "posix" || context.localShellType === "fish")
|
||||
&& !e.isComposing
|
||||
&& e.keyCode !== 229
|
||||
&& isPlainCtrlVPasteChord(e);
|
||||
}
|
||||
@@ -237,3 +237,14 @@ test('normal boot and hibernate wake share terminal link error feedback', () =>
|
||||
/createXTermRuntime\(\{[\s\S]*?onOpenExternalError,/,
|
||||
);
|
||||
});
|
||||
|
||||
test('normal boot and hibernate wake preserve the local shell type for dictation paste', () => {
|
||||
assert.match(
|
||||
effectsSource,
|
||||
/createXTermRuntime\(\{\s*container: containerRef\.current,\s*host,\s*localShellType: ctx\.shellType,/,
|
||||
);
|
||||
assert.match(
|
||||
terminalSource,
|
||||
/xTermRuntimeContextRef\.current = \{\s*host,\s*localShellType: shellType,/,
|
||||
);
|
||||
});
|
||||
@@ -473,6 +473,7 @@ export function useTerminalEffects(ctx: TerminalEffectsContext) {
|
||||
const runtime = createXTermRuntime({
|
||||
container: containerRef.current,
|
||||
host,
|
||||
localShellType: ctx.shellType,
|
||||
fontFamilyId,
|
||||
resolvedFontFamily,
|
||||
fontSize,
|
||||
|
||||
Reference in new issue
Block a user