feat(ui): redesign the default shortcut layout around a mod+k leader

Single chords stay for everyday actions; open/go actions move to two-step
mod+k sequences; held mod+digit switches header session tabs and held
mod+alt+digit switches context panel surfaces. Rare actions leave the
shortcut schema for the command palette, every remaining action ships with
a default binding, and stored overrides from the old layout reset once.
Key matching now follows the physical key on non-Latin layouts and for
Option-modified digits on macOS, including in the recording dialog.
This commit is contained in:
Bohdan Triapitsyn
2026-08-26 14:52:31 +03:00
parent 76f977e580
commit 7977842e1e
44 changed files with 389 additions and 322 deletions
@@ -53,17 +53,19 @@ export const KeyboardShortcutsSettings: React.FC = () => {
persist(nextOverrides);
};
const shortcutDisplay = (action: CustomizableShortcutAction): string => {
const isSurfaceSwitch = action.id === 'switch_context_surface';
const combo = isSurfaceSwitch
const isPrefixStyle = 'prefixStyle' in action && action.prefixStyle;
const combo = isPrefixStyle
? getEffectiveShortcutPrefix(action.id, shortcutOverrides)
: getEffectiveShortcutCombo(action.id, shortcutOverrides);
const formatted = formatShortcutForDisplay(
combo,
t('settings.openchamber.keyboardShortcuts.unassigned'),
);
return isSurfaceSwitch && combo && combo !== UNASSIGNED_SHORTCUT
? `${formatted}${t('settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix')}`
: formatted;
if (!isPrefixStyle || !combo || combo === UNASSIGNED_SHORTCUT) return formatted;
const suffix = action.id === 'switch_session_tab'
? t('settings.openchamber.keyboardShortcuts.action.switch_session_tab.suffix')
: t('settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix');
return `${formatted}${suffix}`;
};
return (
@@ -104,15 +106,17 @@ export const KeyboardShortcutsSettings: React.FC = () => {
>
{t('settings.openchamber.keyboardShortcuts.actions.edit')}
</Button>
<Button
type="button"
variant="ghost"
size="xs"
className="!font-normal"
onClick={() => resetOne(action.id)}
>
{t('settings.common.actions.reset')}
</Button>
{action.id in shortcutOverrides ? (
<Button
type="button"
variant="ghost"
size="xs"
className="!font-normal"
onClick={() => resetOne(action.id)}
>
{t('settings.common.actions.reset')}
</Button>
) : null}
</SettingsFieldRow>
))}
</div>
@@ -4,7 +4,8 @@ import { settleShortcutRecordingState, updateShortcutRecordingState } from './Sh
const emptyState = { chords: [], livePreview: null, settled: false };
function keyEvent(key: string, modifiers: Partial<Record<'altKey' | 'ctrlKey' | 'metaKey' | 'shiftKey', boolean>> = {}) {
return { key, repeat: false, isComposing: false, altKey: false, ctrlKey: false, metaKey: false, shiftKey: false, ...modifiers };
const code = /^[a-z]$/i.test(key) ? `Key${key.toUpperCase()}` : /^[0-9]$/.test(key) ? `Digit${key}` : key;
return { key, code, repeat: false, isComposing: false, altKey: false, ctrlKey: false, metaKey: false, shiftKey: false, ...modifiers };
}
describe('ShortcutRecordingDialog recording state', () => {
@@ -13,6 +13,7 @@ import {
getShortcutBindingConflicts,
isRiskyBrowserShortcut,
keyToShortcutToken,
resolveShortcutEventKey,
normalizeCombo,
type ShortcutActionId,
type ShortcutBindingConflict,
@@ -27,6 +28,7 @@ const SECOND_CHORD_TIMEOUT_MS = 3000;
interface RecordingKeyboardEvent {
altKey: boolean;
code: string;
ctrlKey: boolean;
isComposing: boolean;
key: string;
@@ -84,7 +86,7 @@ function keyboardEventToCombo(event: RecordingKeyboardEvent): ShortcutCombo | nu
if (MODIFIER_KEYS.has(event.key.toLowerCase())) return null;
if (getPhysicalKeyCount(event, true) > MAX_SHORTCUT_KEY_COUNT) return null;
const key = keyToShortcutToken(event.key);
const key = keyToShortcutToken(resolveShortcutEventKey(event));
if (!key) return null;
const parts: string[] = [];
@@ -200,7 +202,8 @@ export const ShortcutRecordingDialog: React.FC<ShortcutRecordingDialogProps> = (
event.preventDefault();
event.stopPropagation();
if (phase === 'keyup' && action?.id === 'switch_context_surface' && recording.chords.length === 0) {
const isPrefixStyleAction = Boolean(action && 'prefixStyle' in action && action.prefixStyle);
if (phase === 'keyup' && isPrefixStyleAction && recording.chords.length === 0) {
const modifierCombo = modifierKeyUpToCombo(event);
if (modifierCombo) {
setRecording({ chords: [modifierCombo], livePreview: null, settled: true });
@@ -209,6 +212,7 @@ export const ShortcutRecordingDialog: React.FC<ShortcutRecordingDialogProps> = (
}
const nextRecording = updateShortcutRecordingState(recording, {
altKey: event.altKey,
code: event.nativeEvent.code,
ctrlKey: event.ctrlKey,
isComposing: event.nativeEvent.isComposing,
key: event.key,
@@ -216,7 +220,7 @@ export const ShortcutRecordingDialog: React.FC<ShortcutRecordingDialogProps> = (
repeat: event.repeat,
shiftKey: event.shiftKey,
}, phase);
setRecording(action?.id === 'switch_context_surface' && nextRecording.chords.length > 1
setRecording(isPrefixStyleAction && nextRecording.chords.length > 1
? recording
: nextRecording);
};