fix: prevent shortcut keys from leaking into TUI editors

Closes #184
This commit is contained in:
Matthew Breedlove committed 2026-02-21 10:44:12 -05:00
1 parent 76d9af84a5
commit d025c707e6
9 files changed
+69 -11

No files matched your search

+3 -2
View File
@@ -15,6 +15,7 @@ import {
getAvailableBackgroundColorsForUI,
getAvailableColorsForUI
} from '../../utils/colors';
import { shouldInsertInput } from '../../utils/input-guards';
import { getWidget } from '../../utils/widgets';
import { ConfirmDialog } from './ConfirmDialog';
@@ -97,7 +98,7 @@ export const ColorMenu: React.FC<ColorMenuProps> = ({ widgets, lineIndex, settin
}
} else if (key.backspace || key.delete) {
setHexInput(hexInput.slice(0, -1));
} else if (input && hexInput.length < 6) {
} else if (shouldInsertInput(input, key) && hexInput.length < 6) {
// Only accept hex characters (0-9, A-F, a-f)
const upperInput = input.toUpperCase();
if (/^[0-9A-F]$/.test(upperInput)) {
@@ -144,7 +145,7 @@ export const ColorMenu: React.FC<ColorMenuProps> = ({ widgets, lineIndex, settin
}
} else if (key.backspace || key.delete) {
setAnsi256Input(ansi256Input.slice(0, -1));
} else if (input && ansi256Input.length < 3) {
} else if (shouldInsertInput(input, key) && ansi256Input.length < 3) {
// Only accept numeric characters (0-9)
if (/^[0-9]$/.test(input)) {
const newInput = ansi256Input + input;
+3 -2
View File
@@ -11,6 +11,7 @@ import {
getChalkColor,
getColorDisplayName
} from '../../utils/colors';
import { shouldInsertInput } from '../../utils/input-guards';
import { ConfirmDialog } from './ConfirmDialog';
@@ -57,7 +58,7 @@ export const GlobalOverridesMenu: React.FC<GlobalOverridesMenuProps> = ({ settin
setPaddingInput(paddingInput.slice(0, -1));
} else if (key.delete) {
// For simple text inputs without cursor, forward delete does nothing
} else if (input) {
} else if (shouldInsertInput(input, key)) {
setPaddingInput(paddingInput + input);
}
} else if (editingSeparator) {
@@ -86,7 +87,7 @@ export const GlobalOverridesMenu: React.FC<GlobalOverridesMenuProps> = ({ settin
setSeparatorInput(separatorInput.slice(0, -1));
} else if (key.delete) {
// For simple text inputs without cursor, forward delete does nothing
} else if (input) {
} else if (shouldInsertInput(input, key)) {
setSeparatorInput(separatorInput + input);
}
} else if (confirmingSeparator) {
@@ -6,6 +6,7 @@ import {
import React, { useState } from 'react';
import type { Settings } from '../../types/Settings';
import { shouldInsertInput } from '../../utils/input-guards';
export type EditorMode = 'separator' | 'startCap' | 'endCap';
@@ -133,7 +134,7 @@ export const PowerlineSeparatorEditor: React.FC<PowerlineSeparatorEditorProps> =
} else if (key.backspace && cursorPos > 0) {
setHexInput(hexInput.slice(0, cursorPos - 1) + hexInput.slice(cursorPos));
setCursorPos(cursorPos - 1);
} else if (input && /[0-9a-fA-F]/.test(input) && hexInput.length < 4) {
} else if (shouldInsertInput(input, key) && /[0-9a-fA-F]/.test(input) && hexInput.length < 4) {
setHexInput(hexInput.slice(0, cursorPos) + input.toUpperCase() + hexInput.slice(cursorPos));
setCursorPos(cursorPos + 1);
}
+2 -1
View File
@@ -7,6 +7,7 @@ import React, { useState } from 'react';
import type { FlexMode } from '../../types/FlexMode';
import type { Settings } from '../../types/Settings';
import { shouldInsertInput } from '../../utils/input-guards';
export interface TerminalWidthMenuProps {
settings: Settings;
@@ -58,7 +59,7 @@ export const TerminalWidthMenu: React.FC<TerminalWidthMenuProps> = ({ settings,
setValidationError(null);
} else if (key.delete) {
// For simple number inputs, forward delete does nothing since there's no cursor position
} else if (input && /\d/.test(input)) {
} else if (shouldInsertInput(input, key) && /\d/.test(input)) {
const newValue = thresholdInput + input;
if (newValue.length <= 2) {
setThresholdInput(newValue);
+31
View File
@@ -0,0 +1,31 @@
import {
describe,
expect,
it
} from 'vitest';
import { shouldInsertInput } from '../input-guards';
describe('shouldInsertInput', () => {
it('allows regular printable input without modifiers', () => {
expect(shouldInsertInput('s', {})).toBe(true);
expect(shouldInsertInput('S', { shift: true })).toBe(true);
});
it('blocks ctrl chords', () => {
expect(shouldInsertInput('s', { ctrl: true })).toBe(false);
});
it('blocks meta chords', () => {
expect(shouldInsertInput('s', { meta: true })).toBe(false);
});
it('blocks tab-based input', () => {
expect(shouldInsertInput('\t', { tab: true })).toBe(false);
});
it('blocks control characters and allows unicode text', () => {
expect(shouldInsertInput('\u0013', {})).toBe(false);
expect(shouldInsertInput('🙂', {})).toBe(true);
});
});
+20
View File
@@ -0,0 +1,20 @@
export interface InputKeyLike {
ctrl?: boolean;
meta?: boolean;
shift?: boolean;
tab?: boolean;
}
const CONTROL_CHAR_REGEX = /[\u0000-\u001F\u007F]/u;
export const shouldInsertInput = (input: string, key: InputKeyLike): boolean => {
if (!input) {
return false;
}
if (key.ctrl || key.meta || key.tab) {
return false;
}
return !CONTROL_CHAR_REGEX.test(input);
};
+2 -1
View File
@@ -15,6 +15,7 @@ import type {
WidgetEditorProps,
WidgetItem
} from '../types/Widget';
import { shouldInsertInput } from '../utils/input-guards';
export class CurrentWorkingDirWidget implements Widget {
getDefaultColor(): string { return 'blue'; }
@@ -200,7 +201,7 @@ const CurrentWorkingDirEditor: React.FC<WidgetEditorProps> = ({ widget, onComple
onCancel();
} else if (key.backspace) {
setSegmentsInput(segmentsInput.slice(0, -1));
} else if (input && /\d/.test(input) && !key.ctrl) {
} else if (shouldInsertInput(input, key) && /\d/.test(input)) {
setSegmentsInput(segmentsInput + input);
}
}
+4 -3
View File
@@ -15,6 +15,7 @@ import type {
WidgetEditorProps,
WidgetItem
} from '../types/Widget';
import { shouldInsertInput } from '../utils/input-guards';
export class CustomCommandWidget implements Widget {
getDefaultColor(): string { return 'white'; }
@@ -160,7 +161,7 @@ const CustomCommandEditor: React.FC<WidgetEditorProps> = ({ widget, onComplete,
if (commandCursorPos < commandInput.length) {
setCommandInput(commandInput.slice(0, commandCursorPos) + commandInput.slice(commandCursorPos + 1));
}
} else if (input) {
} else if (shouldInsertInput(input, key)) {
setCommandInput(commandInput.slice(0, commandCursorPos) + input + commandInput.slice(commandCursorPos));
setCommandCursorPos(commandCursorPos + input.length);
}
@@ -178,7 +179,7 @@ const CustomCommandEditor: React.FC<WidgetEditorProps> = ({ widget, onComplete,
onCancel();
} else if (key.backspace) {
setWidthInput(widthInput.slice(0, -1));
} else if (input && /\d/.test(input)) {
} else if (shouldInsertInput(input, key) && /\d/.test(input)) {
setWidthInput(widthInput + input);
}
} else if (mode === 'timeout') {
@@ -195,7 +196,7 @@ const CustomCommandEditor: React.FC<WidgetEditorProps> = ({ widget, onComplete,
onCancel();
} else if (key.backspace) {
setTimeoutInput(timeoutInput.slice(0, -1));
} else if (input && /\d/.test(input)) {
} else if (shouldInsertInput(input, key) && /\d/.test(input)) {
setTimeoutInput(timeoutInput + input);
}
}
+2 -1
View File
@@ -14,6 +14,7 @@ import type {
WidgetEditorProps,
WidgetItem
} from '../types/Widget';
import { shouldInsertInput } from '../utils/input-guards';
export class CustomTextWidget implements Widget {
getDefaultColor(): string { return 'white'; }
@@ -129,7 +130,7 @@ const CustomTextEditor: React.FC<WidgetEditorProps> = ({ widget, onComplete, onC
setText(text.slice(0, deleteFromIndex) + text.slice(deleteToIndex));
}
}
} else if (input && !key.ctrl && !key.meta) {
} else if (shouldInsertInput(input, key)) {
// Insert the input at cursor position
const newText = text.slice(0, cursorPos) + input + text.slice(cursorPos);
setText(newText);