mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-10-30 10:22:33 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
65f20b899b | ||
|
|
3068bd9ffa | ||
|
|
755eab661d |
No files matched your search
@@ -10,12 +10,14 @@ import {
|
||||
resolveThemeAccentForeground,
|
||||
migrateIncomingTerminalFontId,
|
||||
migrateMaterializedAutoCloseOnExitDefault,
|
||||
migrateMaterializedShiftEnterNewlineDefault,
|
||||
} from "./settingsStateDefaults.ts";
|
||||
import { TERMINAL_FONT_AUTO } from "../../infrastructure/config/fonts.ts";
|
||||
import {
|
||||
STORAGE_KEY_TERM_AUTO_CLOSE_EXIT_MIGRATED,
|
||||
STORAGE_KEY_TERM_FONT_FAMILY,
|
||||
STORAGE_KEY_TERM_SETTINGS,
|
||||
STORAGE_KEY_TERM_SHIFT_ENTER_NEWLINE_MIGRATED,
|
||||
} from "../../infrastructure/config/storageKeys.ts";
|
||||
import type { UiThemeTokens } from "../../infrastructure/config/uiThemes.ts";
|
||||
|
||||
@@ -214,3 +216,76 @@ test("migrateMaterializedAutoCloseOnExitDefault leaves fresh installs and explic
|
||||
delete (globalThis as { localStorage?: unknown }).localStorage;
|
||||
}
|
||||
});
|
||||
|
||||
test("migrateMaterializedShiftEnterNewlineDefault rewrites the old default once", () => {
|
||||
const store = installMemoryLocalStorage();
|
||||
try {
|
||||
store.set(
|
||||
STORAGE_KEY_TERM_SETTINGS,
|
||||
JSON.stringify({ scrollback: 3000, shiftEnterNewlineText: "\\n" }),
|
||||
);
|
||||
migrateMaterializedShiftEnterNewlineDefault();
|
||||
const migrated = JSON.parse(store.get(STORAGE_KEY_TERM_SETTINGS)!);
|
||||
assert.equal(migrated.shiftEnterNewlineText, "\\e\\r");
|
||||
assert.equal(migrated.scrollback, 3000);
|
||||
assert.equal(store.get(STORAGE_KEY_TERM_SHIFT_ENTER_NEWLINE_MIGRATED), "true");
|
||||
|
||||
store.set(
|
||||
STORAGE_KEY_TERM_SETTINGS,
|
||||
JSON.stringify({ scrollback: 3000, shiftEnterNewlineText: "\\n" }),
|
||||
);
|
||||
migrateMaterializedShiftEnterNewlineDefault();
|
||||
assert.equal(
|
||||
JSON.parse(store.get(STORAGE_KEY_TERM_SETTINGS)!).shiftEnterNewlineText,
|
||||
"\\n",
|
||||
);
|
||||
} finally {
|
||||
delete (globalThis as { localStorage?: unknown }).localStorage;
|
||||
}
|
||||
});
|
||||
|
||||
test("migrateMaterializedShiftEnterNewlineDefault rewrites a raw stored newline", () => {
|
||||
const store = installMemoryLocalStorage();
|
||||
try {
|
||||
store.set(
|
||||
STORAGE_KEY_TERM_SETTINGS,
|
||||
JSON.stringify({ shiftEnterNewlineText: "\n" }),
|
||||
);
|
||||
migrateMaterializedShiftEnterNewlineDefault();
|
||||
assert.equal(
|
||||
JSON.parse(store.get(STORAGE_KEY_TERM_SETTINGS)!).shiftEnterNewlineText,
|
||||
"\\e\\r",
|
||||
);
|
||||
} finally {
|
||||
delete (globalThis as { localStorage?: unknown }).localStorage;
|
||||
}
|
||||
});
|
||||
|
||||
test("migrateMaterializedShiftEnterNewlineDefault keeps custom Shift+Enter text", () => {
|
||||
const store = installMemoryLocalStorage();
|
||||
try {
|
||||
store.set(
|
||||
STORAGE_KEY_TERM_SETTINGS,
|
||||
JSON.stringify({ shiftEnterNewlineText: "\\r", scrollback: 1000 }),
|
||||
);
|
||||
migrateMaterializedShiftEnterNewlineDefault();
|
||||
assert.deepEqual(JSON.parse(store.get(STORAGE_KEY_TERM_SETTINGS)!), {
|
||||
shiftEnterNewlineText: "\\r",
|
||||
scrollback: 1000,
|
||||
});
|
||||
assert.equal(store.get(STORAGE_KEY_TERM_SHIFT_ENTER_NEWLINE_MIGRATED), "true");
|
||||
} finally {
|
||||
delete (globalThis as { localStorage?: unknown }).localStorage;
|
||||
}
|
||||
});
|
||||
|
||||
test("migrateMaterializedShiftEnterNewlineDefault leaves a fresh install without a settings blob", () => {
|
||||
const store = installMemoryLocalStorage();
|
||||
try {
|
||||
migrateMaterializedShiftEnterNewlineDefault();
|
||||
assert.equal(store.has(STORAGE_KEY_TERM_SETTINGS), false);
|
||||
assert.equal(store.get(STORAGE_KEY_TERM_SHIFT_ENTER_NEWLINE_MIGRATED), "true");
|
||||
} finally {
|
||||
delete (globalThis as { localStorage?: unknown }).localStorage;
|
||||
}
|
||||
});
|
||||
@@ -1,8 +1,15 @@
|
||||
import type { HotkeyScheme, SessionLogFormat, TerminalSettings } from '../../domain/models';
|
||||
import {
|
||||
DEFAULT_SHIFT_ENTER_NEWLINE_TEXT,
|
||||
isLegacyDefaultShiftEnterNewlineText,
|
||||
type HotkeyScheme,
|
||||
type SessionLogFormat,
|
||||
type TerminalSettings,
|
||||
} from '../../domain/models';
|
||||
import {
|
||||
STORAGE_KEY_TERM_AUTO_CLOSE_EXIT_MIGRATED,
|
||||
STORAGE_KEY_TERM_FONT_FAMILY,
|
||||
STORAGE_KEY_TERM_SETTINGS,
|
||||
STORAGE_KEY_TERM_SHIFT_ENTER_NEWLINE_MIGRATED,
|
||||
} from '../../infrastructure/config/storageKeys';
|
||||
import {
|
||||
isDeprecatedPrimaryFontId,
|
||||
@@ -91,6 +98,26 @@ export function migrateMaterializedAutoCloseOnExitDefault(): void {
|
||||
localStorageAdapter.writeBoolean(STORAGE_KEY_TERM_AUTO_CLOSE_EXIT_MIGRATED, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* One-time migration for the Shift+Enter default flip (#3770). Builds before
|
||||
* that change materialized `\\n` into saved settings, so upgraders kept a
|
||||
* bare newline and TUI CLIs still submitted. Rewrite only that old default
|
||||
* (escaped or raw) to ESC+CR, then persist the marker. A later explicit `\\n`
|
||||
* is left alone. Custom text is never rewritten. Sync and import do not
|
||||
* read this marker; an old `\\n` from another device stays as stored.
|
||||
*/
|
||||
export function migrateMaterializedShiftEnterNewlineDefault(): void {
|
||||
if (localStorageAdapter.readBoolean(STORAGE_KEY_TERM_SHIFT_ENTER_NEWLINE_MIGRATED)) return;
|
||||
const stored = localStorageAdapter.read<Partial<TerminalSettings>>(STORAGE_KEY_TERM_SETTINGS);
|
||||
if (stored && isLegacyDefaultShiftEnterNewlineText(stored.shiftEnterNewlineText)) {
|
||||
localStorageAdapter.write(STORAGE_KEY_TERM_SETTINGS, {
|
||||
...stored,
|
||||
shiftEnterNewlineText: DEFAULT_SHIFT_ENTER_NEWLINE_TEXT,
|
||||
});
|
||||
}
|
||||
localStorageAdapter.writeBoolean(STORAGE_KEY_TERM_SHIFT_ENTER_NEWLINE_MIGRATED, true);
|
||||
}
|
||||
|
||||
// Auto-detect default hotkey scheme based on platform
|
||||
export const DEFAULT_HOTKEY_SCHEME: HotkeyScheme =
|
||||
typeof navigator !== 'undefined' && /Mac|iPhone|iPad|iPod/i.test(navigator.platform)
|
||||
|
||||
@@ -151,6 +151,7 @@ import {
|
||||
isValidUiThemeId,
|
||||
migrateIncomingTerminalFontId,
|
||||
migrateMaterializedAutoCloseOnExitDefault,
|
||||
migrateMaterializedShiftEnterNewlineDefault,
|
||||
readStoredString,
|
||||
serializeTerminalSettings,
|
||||
type HostClickBehavior,
|
||||
@@ -360,6 +361,7 @@ export const useSettingsState = (options: { enableSettingsSync?: boolean; enable
|
||||
// One-time reset of the pre-flip materialized autoCloseOnExit: true before
|
||||
// it is ingested; afterwards stored values are the user's own and win.
|
||||
migrateMaterializedAutoCloseOnExitDefault();
|
||||
migrateMaterializedShiftEnterNewlineDefault();
|
||||
const stored = localStorageAdapter.read<TerminalSettings>(STORAGE_KEY_TERM_SETTINGS);
|
||||
return normalizeTerminalSettings(stored);
|
||||
});
|
||||
|
||||
@@ -114,6 +114,8 @@ import { handleSerialLineModeInput } from "./serialLineInput";
|
||||
import { isTerminalReportSequence } from "./terminalReportSequence";
|
||||
import {
|
||||
getShiftEnterSubmittedInput,
|
||||
nativeKittyKeyboardPreservesShiftEnter,
|
||||
readNativeKittyKeyboardFlags,
|
||||
resolveShiftEnterText,
|
||||
shouldClaimShiftEnterForText,
|
||||
shouldSendShiftEnterText,
|
||||
@@ -596,8 +598,12 @@ export const createXTermRuntime = (ctx: CreateXTermRuntimeContext): XTermRuntime
|
||||
// ConPTY asks the frontend for Win32 INPUT_RECORD encoding with DECSET
|
||||
// 9001. Preserve browser key modifiers for native Windows applications
|
||||
// instead of collapsing them into legacy VT bytes.
|
||||
// kittyKeyboard lets an app opt into xterm's CSI u. Netcatty's own
|
||||
// handlers, when that setting is on, register later and consume those
|
||||
// sequences first, so the two parsers do not both apply the protocol.
|
||||
vtExtensions: {
|
||||
win32InputMode: windowsPty?.backend === "conpty",
|
||||
kittyKeyboard: true,
|
||||
},
|
||||
// Override ignoreBracketedPasteMode if user explicitly disables bracketed paste
|
||||
ignoreBracketedPasteMode: settings?.disableBracketedPaste ?? performanceConfig.options.ignoreBracketedPasteMode,
|
||||
@@ -2528,6 +2534,8 @@ export const createXTermRuntime = (ctx: CreateXTermRuntimeContext): XTermRuntime
|
||||
shouldClaimShiftEnterForText(e, ctx.terminalSettingsRef.current, {
|
||||
win32InputMode: term.modes.win32InputMode,
|
||||
kittySequenceForKeyDown,
|
||||
nativeKittyPreservesShiftEnter: !kittyKeyboardProtocolEnabled
|
||||
&& nativeKittyKeyboardPreservesShiftEnter(readNativeKittyKeyboardFlags(term)),
|
||||
})
|
||||
) {
|
||||
const id = ctx.sessionRef.current;
|
||||
@@ -3123,6 +3131,8 @@ export const createXTermRuntime = (ctx: CreateXTermRuntimeContext): XTermRuntime
|
||||
legacySuppressedKeys: broadcastLegacySuppressedKeys,
|
||||
win32InputMode: term.modes.win32InputMode,
|
||||
shiftEnterSettings: ctx.terminalSettingsRef.current,
|
||||
nativeKittyPreservesShiftEnter: !kittyKeyboardProtocolEnabled
|
||||
&& nativeKittyKeyboardPreservesShiftEnter(readNativeKittyKeyboardFlags(term)),
|
||||
win32ShiftEnterTextKeys: broadcastWin32ShiftEnterTextKeys,
|
||||
}),
|
||||
getSessionId: () => ctx.sessionRef.current,
|
||||
|
||||
@@ -571,6 +571,61 @@ test("Shift+Enter broadcast uses CSI-u only after the target negotiated a preser
|
||||
});
|
||||
});
|
||||
|
||||
test("native kitty broadcast targets keep Shift+Enter as CSI-u when custom kitty is off", () => {
|
||||
const shiftEnter = {
|
||||
kind: "key" as const,
|
||||
fallbackToLegacy: true,
|
||||
event: {
|
||||
type: "keydown" as const,
|
||||
key: "Enter",
|
||||
code: "Enter",
|
||||
shiftKey: true,
|
||||
},
|
||||
};
|
||||
const encodedKeys = new Set<string>();
|
||||
const legacySuppressedKeys = new Set<string>();
|
||||
const base = {
|
||||
kittyProtocolEnabled: false,
|
||||
kittyMode: createKittyKeyboardModeState(),
|
||||
applicationCursorMode: false,
|
||||
shiftEnterSettings: {
|
||||
shiftEnterNewlineEnabled: true,
|
||||
shiftEnterNewlineText: "\\e\\r",
|
||||
},
|
||||
};
|
||||
|
||||
assert.deepEqual(resolveKittyKeyboardBroadcastInput(shiftEnter, {
|
||||
...base,
|
||||
encodedKeys,
|
||||
legacySuppressedKeys,
|
||||
nativeKittyPreservesShiftEnter: true,
|
||||
}), {
|
||||
data: SHIFT_ENTER_CSI_U_SEQUENCE,
|
||||
kittyEncoded: true,
|
||||
urgentInterrupt: false,
|
||||
});
|
||||
assert.equal(resolveKittyKeyboardBroadcastInput({
|
||||
kind: "legacy",
|
||||
data: SHIFT_ENTER_CSI_U_SEQUENCE,
|
||||
keyIdentity: "Enter",
|
||||
}, {
|
||||
...base,
|
||||
encodedKeys,
|
||||
legacySuppressedKeys,
|
||||
nativeKittyPreservesShiftEnter: true,
|
||||
}), null);
|
||||
|
||||
assert.deepEqual(resolveKittyKeyboardBroadcastInput(shiftEnter, {
|
||||
...base,
|
||||
encodedKeys: new Set<string>(),
|
||||
nativeKittyPreservesShiftEnter: false,
|
||||
}), {
|
||||
data: "\u001b\r",
|
||||
kittyEncoded: false,
|
||||
urgentInterrupt: false,
|
||||
});
|
||||
});
|
||||
|
||||
test("Win32 broadcast targets resolve Shift+Enter from their own force-text setting", () => {
|
||||
const shiftEnterRecord = "\u001b[13;28;13;1;16;1_";
|
||||
const press = { type: "keydown" as const, key: "Enter", code: "Enter", shiftKey: true };
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
isBareShiftEnterLineEnding,
|
||||
resolveShiftEnterText,
|
||||
resolveWin32ForcedShiftEnterText,
|
||||
SHIFT_ENTER_CSI_U_SEQUENCE,
|
||||
shouldSendShiftEnterText,
|
||||
} from "./shiftEnterText";
|
||||
|
||||
@@ -253,6 +254,11 @@ type ResolveKittyKeyboardBroadcastOptions = {
|
||||
TerminalSettings,
|
||||
"shiftEnterNewlineEnabled" | "shiftEnterNewlineText" | "shiftEnterForceText"
|
||||
>;
|
||||
/**
|
||||
* Target xterm negotiated native kitty flags that encode Shift+Enter as
|
||||
* CSI 13;2u, while Netcatty's custom kitty setting is off.
|
||||
*/
|
||||
nativeKittyPreservesShiftEnter?: boolean;
|
||||
/**
|
||||
* Presses this Win32 target turned into its configured Shift+Enter text
|
||||
* instead of a native record. Their paired releases are dropped so ConPTY
|
||||
@@ -418,6 +424,18 @@ export const resolveKittyKeyboardBroadcastInput = (
|
||||
if (remapped) return remapped;
|
||||
return { data: encoded, kittyEncoded: true, urgentInterrupt: false };
|
||||
}
|
||||
// Custom kitty is off. A target whose xterm already negotiated a preserving
|
||||
// native mode must get CSI 13;2u, and the source's follow-up legacy packet
|
||||
// (the same sequence) must not be written again.
|
||||
if (options.nativeKittyPreservesShiftEnter && shouldSendShiftEnterText(input.event)) {
|
||||
options.encodedKeys.add(identity);
|
||||
legacySuppressedKeys.add(identity);
|
||||
return {
|
||||
data: SHIFT_ENTER_CSI_U_SEQUENCE,
|
||||
kittyEncoded: true,
|
||||
urgentInterrupt: false,
|
||||
};
|
||||
}
|
||||
if (!input.fallbackToLegacy || input.event.type === "keyup") return null;
|
||||
const legacy = encodeLegacyKeyboardEvent(input.event, options.applicationCursorMode);
|
||||
if (!legacy) return null;
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import xterm from "@xterm/xterm";
|
||||
|
||||
import {
|
||||
nativeKittyKeyboardPreservesShiftEnter,
|
||||
readNativeKittyKeyboardFlags,
|
||||
SHIFT_ENTER_CSI_U_SEQUENCE,
|
||||
} from "./shiftEnterText.ts";
|
||||
|
||||
const { Terminal } = xterm;
|
||||
|
||||
type KittyProbe = {
|
||||
flags: number;
|
||||
shiftEnter: string | undefined;
|
||||
};
|
||||
|
||||
const probeShiftEnter = (setup: string): Promise<KittyProbe> => {
|
||||
const term = new Terminal({
|
||||
cols: 80,
|
||||
rows: 24,
|
||||
allowProposedApi: true,
|
||||
vtExtensions: { kittyKeyboard: true },
|
||||
});
|
||||
return new Promise((resolve) => {
|
||||
term.write(setup, () => {
|
||||
const event = {
|
||||
type: "keydown",
|
||||
key: "Enter",
|
||||
code: "Enter",
|
||||
shiftKey: true,
|
||||
altKey: false,
|
||||
ctrlKey: false,
|
||||
metaKey: false,
|
||||
repeat: false,
|
||||
keyCode: 13,
|
||||
};
|
||||
const core = (term as unknown as {
|
||||
_core: {
|
||||
_keyboardService: {
|
||||
evaluateKeyDown: (ev: object) => { key?: string };
|
||||
};
|
||||
};
|
||||
})._core;
|
||||
resolve({
|
||||
flags: readNativeKittyKeyboardFlags(term),
|
||||
shiftEnter: core._keyboardService.evaluateKeyDown(event).key,
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
test("xterm kitty keyboard encodes opted-in Shift+Enter as CSI 13;2u", async () => {
|
||||
const idle = await probeShiftEnter("");
|
||||
assert.equal(idle.flags, 0);
|
||||
assert.equal(idle.shiftEnter, "\r");
|
||||
assert.equal(nativeKittyKeyboardPreservesShiftEnter(idle.flags), false);
|
||||
|
||||
const disambiguate = await probeShiftEnter("\u001b[=1u");
|
||||
assert.equal(disambiguate.flags, 1);
|
||||
assert.equal(disambiguate.shiftEnter, SHIFT_ENTER_CSI_U_SEQUENCE);
|
||||
assert.equal(nativeKittyKeyboardPreservesShiftEnter(disambiguate.flags), true);
|
||||
|
||||
const alternateOnly = await probeShiftEnter("\u001b[=4u");
|
||||
assert.equal(alternateOnly.flags, 4);
|
||||
assert.equal(alternateOnly.shiftEnter, "\r");
|
||||
assert.equal(nativeKittyKeyboardPreservesShiftEnter(alternateOnly.flags), false);
|
||||
});
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
decodeTerminalTextEscapes,
|
||||
doesKittyEncodingPreserveShiftEnter,
|
||||
getShiftEnterSubmittedInput,
|
||||
nativeKittyKeyboardPreservesShiftEnter,
|
||||
isBareShiftEnterLineEnding,
|
||||
isShiftEnterLineContinuationText,
|
||||
resolveShiftEnterText,
|
||||
@@ -161,6 +162,41 @@ test("force-text opt-out only overrides the Win32 hand-off", () => {
|
||||
assert.equal(shouldClaimShiftEnterForText(keyEvent({ ctrlKey: true }), forced, win32), false);
|
||||
});
|
||||
|
||||
test("native kitty Shift+Enter steps aside only for CSI-u flag sets", () => {
|
||||
assert.equal(nativeKittyKeyboardPreservesShiftEnter(0), false);
|
||||
assert.equal(nativeKittyKeyboardPreservesShiftEnter(0b0001), true);
|
||||
assert.equal(nativeKittyKeyboardPreservesShiftEnter(0b0010), true);
|
||||
assert.equal(nativeKittyKeyboardPreservesShiftEnter(0b1000), true);
|
||||
assert.equal(nativeKittyKeyboardPreservesShiftEnter(0b0100), false);
|
||||
assert.equal(nativeKittyKeyboardPreservesShiftEnter(0b10000), false);
|
||||
|
||||
const settings = { shiftEnterNewlineEnabled: true, shiftEnterForceText: false };
|
||||
assert.equal(
|
||||
shouldClaimShiftEnterForText(keyEvent(), settings, {
|
||||
win32InputMode: false,
|
||||
kittySequenceForKeyDown: null,
|
||||
nativeKittyPreservesShiftEnter: true,
|
||||
}),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
shouldClaimShiftEnterForText(keyEvent(), settings, {
|
||||
win32InputMode: false,
|
||||
kittySequenceForKeyDown: null,
|
||||
nativeKittyPreservesShiftEnter: false,
|
||||
}),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
shouldClaimShiftEnterForText(keyEvent(), { ...settings, shiftEnterForceText: true }, {
|
||||
win32InputMode: true,
|
||||
kittySequenceForKeyDown: null,
|
||||
nativeKittyPreservesShiftEnter: true,
|
||||
}),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test("Win32 forced Shift+Enter text requires the opt-out and non-empty text", () => {
|
||||
const settings = {
|
||||
shiftEnterNewlineEnabled: true,
|
||||
@@ -193,7 +229,7 @@ test("runtime routes Shift+Enter text through the shared input handler", () => {
|
||||
// skipping the ConPTY Win32 hand-off only when the force-text opt-out is on.
|
||||
assert.match(
|
||||
source,
|
||||
/if \(\s*shouldClaimShiftEnterForText\(e, ctx\.terminalSettingsRef\.current, \{\s*win32InputMode: term\.modes\.win32InputMode,\s*kittySequenceForKeyDown,\s*\}\)\s*\) \{[\s\S]*?const shiftEnterText = resolveShiftEnterText\([\s\S]*?\)[\s\S]*?\}\s*if \(kittySequenceForKeyDown\)/s,
|
||||
/if \(\s*shouldClaimShiftEnterForText\(e, ctx\.terminalSettingsRef\.current, \{\s*win32InputMode: term\.modes\.win32InputMode,\s*kittySequenceForKeyDown,\s*nativeKittyPreservesShiftEnter: !kittyKeyboardProtocolEnabled\s*&& nativeKittyKeyboardPreservesShiftEnter\(readNativeKittyKeyboardFlags\(term\)\),\s*\}\)\s*\) \{[\s\S]*?const shiftEnterText = resolveShiftEnterText\([\s\S]*?\)[\s\S]*?\}\s*if \(kittySequenceForKeyDown\)/s,
|
||||
);
|
||||
// The gate above only decides whether Shift+Enter is *eligible* for send-text.
|
||||
// It must then claim the keydown only when resolveShiftEnterText() returned
|
||||
@@ -221,7 +257,7 @@ test("runtime routes Shift+Enter text through the shared input handler", () => {
|
||||
);
|
||||
assert.match(
|
||||
source,
|
||||
/vtExtensions: \{\s*win32InputMode: windowsPty\?\.backend === "conpty",\s*\},/s,
|
||||
/vtExtensions: \{\s*win32InputMode: windowsPty\?\.backend === "conpty",\s*kittyKeyboard: true,\s*\},/s,
|
||||
);
|
||||
assert.match(
|
||||
source,
|
||||
@@ -237,7 +273,7 @@ test("runtime routes Shift+Enter text through the shared input handler", () => {
|
||||
);
|
||||
assert.match(
|
||||
source,
|
||||
/resolveOptions: \(\) => \(\{[\s\S]*?shiftEnterSettings: ctx\.terminalSettingsRef\.current,[\s\S]*?\}\),/s,
|
||||
/resolveOptions: \(\) => \(\{[\s\S]*?shiftEnterSettings: ctx\.terminalSettingsRef\.current,[\s\S]*?nativeKittyPreservesShiftEnter: !kittyKeyboardProtocolEnabled\s*&& nativeKittyKeyboardPreservesShiftEnter\(readNativeKittyKeyboardFlags\(term\)\),[\s\S]*?\}\),/s,
|
||||
);
|
||||
assert.doesNotMatch(source, /resolveShiftEnterText\([\s\S]*?alternateScreen:/s);
|
||||
assert.match(source, /getShiftEnterSubmittedInput\(logicalData\)/);
|
||||
|
||||
@@ -100,6 +100,24 @@ export function doesKittyEncodingPreserveShiftEnter(
|
||||
&& !isBareShiftEnterLineEnding(encoded);
|
||||
}
|
||||
|
||||
/**
|
||||
* xterm 6.1 flag bits that encode Shift+Enter as CSI 13;2u.
|
||||
* Disambiguate (1), report event types (2), and report all keys (8).
|
||||
* Alternate-key (4) or associated-text (16) alone still emit a bare CR.
|
||||
*/
|
||||
const NATIVE_KITTY_SHIFT_ENTER_CSI_U_FLAGS = 0b0001 | 0b0010 | 0b1000;
|
||||
|
||||
export function nativeKittyKeyboardPreservesShiftEnter(flags: number): boolean {
|
||||
return (flags & NATIVE_KITTY_SHIFT_ENTER_CSI_U_FLAGS) !== 0;
|
||||
}
|
||||
|
||||
export function readNativeKittyKeyboardFlags(term: {
|
||||
_core?: { coreService?: { kittyKeyboard?: { flags?: unknown } } };
|
||||
}): number {
|
||||
const flags = term._core?.coreService?.kittyKeyboard?.flags;
|
||||
return typeof flags === "number" && Number.isFinite(flags) ? flags : 0;
|
||||
}
|
||||
|
||||
export function isShiftEnterLineContinuationText(text: string): boolean {
|
||||
return /\\(?:\r\n|\r|\n)$/.test(text);
|
||||
}
|
||||
@@ -150,6 +168,8 @@ export function resolveWin32ForcedShiftEnterText(
|
||||
* opted out with shiftEnterForceText; elsewhere the text is used only when the
|
||||
* negotiated Kitty encoding would collapse the chord to a bare CR/LF, so the
|
||||
* Win32 opt-out can never override a Kitty encoding that preserves it.
|
||||
* xterm's own CSI-u mode is the same kind of preserving encoding: step aside
|
||||
* so the app receives CSI 13;2u.
|
||||
*/
|
||||
export function shouldClaimShiftEnterForText(
|
||||
event: ShiftEnterEvent,
|
||||
@@ -157,10 +177,15 @@ export function shouldClaimShiftEnterForText(
|
||||
TerminalSettings,
|
||||
"shiftEnterNewlineEnabled" | "shiftEnterForceText"
|
||||
> | undefined,
|
||||
mode: { win32InputMode: boolean; kittySequenceForKeyDown: string | null },
|
||||
mode: {
|
||||
win32InputMode: boolean;
|
||||
kittySequenceForKeyDown: string | null;
|
||||
nativeKittyPreservesShiftEnter?: boolean;
|
||||
},
|
||||
): boolean {
|
||||
if (!shouldSendShiftEnterText(event, settings)) return false;
|
||||
return mode.win32InputMode
|
||||
? settings?.shiftEnterForceText === true
|
||||
: !doesKittyEncodingPreserveShiftEnter(mode.kittySequenceForKeyDown);
|
||||
if (mode.win32InputMode) return settings?.shiftEnterForceText === true;
|
||||
if (doesKittyEncodingPreserveShiftEnter(mode.kittySequenceForKeyDown)) return false;
|
||||
if (mode.nativeKittyPreservesShiftEnter) return false;
|
||||
return true;
|
||||
}
|
||||
@@ -503,6 +503,17 @@ export const normalizeTerminalSettings = (
|
||||
/** Default scrollback rows for new installs (VS Code uses 1000; we keep a modest headroom). */
|
||||
export const DEFAULT_TERMINAL_SCROLLBACK = 3000;
|
||||
|
||||
/** Backslash-escaped Shift+Enter text for a fresh install (legacy Alt+Enter). */
|
||||
export const DEFAULT_SHIFT_ENTER_NEWLINE_TEXT = '\\e\\r';
|
||||
|
||||
/**
|
||||
* Stored forms of the pre-#3770 Shift+Enter default. `\\n` is the escaped
|
||||
* setting string; `\n` is a raw newline if one was ever persisted.
|
||||
*/
|
||||
export function isLegacyDefaultShiftEnterNewlineText(value: unknown): boolean {
|
||||
return value === '\\n' || value === '\n';
|
||||
}
|
||||
|
||||
const DEFAULT_TERMINAL_SETTINGS: TerminalSettings = {
|
||||
scrollback: DEFAULT_TERMINAL_SCROLLBACK,
|
||||
drawBoldInBrightColors: true,
|
||||
@@ -522,7 +533,7 @@ const DEFAULT_TERMINAL_SETTINGS: TerminalSettings = {
|
||||
altAsMeta: false,
|
||||
optionArrowWordJump: false,
|
||||
shiftEnterNewlineEnabled: true,
|
||||
shiftEnterNewlineText: '\\e\\r',
|
||||
shiftEnterNewlineText: DEFAULT_SHIFT_ENTER_NEWLINE_TEXT,
|
||||
shiftEnterForceText: false,
|
||||
kittyKeyboardProtocolEnabled: false,
|
||||
scrollOnInput: true,
|
||||
|
||||
@@ -111,6 +111,17 @@ test("normalizeTerminalSettings preserves Shift+Enter text", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("normalizeTerminalSettings keeps an explicit legacy Shift+Enter newline", () => {
|
||||
assert.equal(
|
||||
normalizeTerminalSettings({ shiftEnterNewlineText: "\\n" }).shiftEnterNewlineText,
|
||||
"\\n",
|
||||
);
|
||||
assert.equal(
|
||||
normalizeTerminalSettings({ shiftEnterNewlineText: "\n" }).shiftEnterNewlineText,
|
||||
"\n",
|
||||
);
|
||||
});
|
||||
|
||||
test("normalizeTerminalSettings falls back when Shift+Enter text is not a string", () => {
|
||||
assert.equal(
|
||||
normalizeTerminalSettings({ shiftEnterNewlineText: null as never }).shiftEnterNewlineText,
|
||||
|
||||
@@ -26,6 +26,8 @@ export const STORAGE_KEY_TERM_FONT_SIZE = 'netcatty_term_font_size_v1';
|
||||
export const STORAGE_KEY_TERM_SETTINGS = 'netcatty_term_settings_v1';
|
||||
/** One-time marker: pre-#3619 builds materialized autoCloseOnExit: true into saved settings. */
|
||||
export const STORAGE_KEY_TERM_AUTO_CLOSE_EXIT_MIGRATED = 'netcatty_term_auto_close_exit_migrated_v1';
|
||||
/** One-time marker: pre-#3770 builds materialized shiftEnterNewlineText "\\n" into saved settings. */
|
||||
export const STORAGE_KEY_TERM_SHIFT_ENTER_NEWLINE_MIGRATED = 'netcatty_term_shift_enter_newline_migrated_v1';
|
||||
export const STORAGE_KEY_HOTKEY_SCHEME = 'netcatty_hotkey_scheme_v1';
|
||||
export const STORAGE_KEY_CUSTOM_KEY_BINDINGS = 'netcatty_custom_key_bindings_v1';
|
||||
export const STORAGE_KEY_HOTKEY_RECORDING = 'netcatty_hotkey_recording_v1';
|
||||
|
||||
Reference in new issue
Block a user