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:
@@ -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);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user