Compare commits

...
Author SHA1 Message Date
Cursor Agentand陈大猫 65f20b899b fix(terminal): keep native kitty Shift+Enter on broadcast peers
When a target has negotiated xterm kitty keyboard and the custom
protocol setting is off, resolve the broadcast chord as CSI 13;2u
instead of the ESC+CR fallback.

Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
2026-10-20 04:23:32 +00:00
Cursor Agentand陈大猫 3068bd9ffa fix(terminal): keep the Shift+Enter migration local
The one-time rewrite of a saved "\n" does not need a sync flag or a
three-way merge. An imported or synced old newline can stay as stored.

Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
2026-10-20 04:23:32 +00:00
Cursor Agentand陈大猫 755eab661d fix(terminal): migrate the saved Shift+Enter newline once
Existing installs kept the pre-#3770 "\\n" default, so Shift+Enter still
submitted in TUI CLIs. Rewrite only that stored value to ESC+CR, and let
xterm's native CSI u own the chord when an app has opted in.

Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
2026-10-20 03:44:34 +00:00
12 changed files with 349 additions and 9 deletions

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;
}
});
+28 -1
View File
@@ -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)
+2
View File
@@ -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\)/);
+29 -4
View File
@@ -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;
}
+12 -1
View File
@@ -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,
+11
View File
@@ -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,
+2
View File
@@ -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';