Add keyboard shortcut rebinding with string-based format and sectioned settings UI
- Parse/format shortcut strings ("Cmd+Shift+O") bidirectionally via shortcut-string.ts
- Convert all 60+ bindings from structured KeyCombo objects to declarative strings
- Add keyboard shortcut overrides storage (AsyncStorage + React Query)
- Wire overrides into the keyboard shortcut resolution pipeline
- Add sectioned settings layout with sidebar on desktop, single scroll on mobile
- Add keyboard shortcuts settings section with rebind/reset UI and key capture
- Create useShortcutKeys hook so tooltip shortcut displays update on rebind
- Replace hardcoded shortcut displays in agent-input-area, left-sidebar, command center
This commit is contained in:
@@ -28,6 +28,7 @@ import { useVoiceOptional } from "@/contexts/voice-context";
|
|||||||
import { useToast } from "@/contexts/toast-context";
|
import { useToast } from "@/contexts/toast-context";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
import { Shortcut } from "@/components/ui/shortcut";
|
import { Shortcut } from "@/components/ui/shortcut";
|
||||||
|
import { useShortcutKeys } from "@/hooks/use-shortcut-keys";
|
||||||
import { Autocomplete } from "@/components/ui/autocomplete";
|
import { Autocomplete } from "@/components/ui/autocomplete";
|
||||||
import { useAgentAutocomplete } from "@/hooks/use-agent-autocomplete";
|
import { useAgentAutocomplete } from "@/hooks/use-agent-autocomplete";
|
||||||
import {
|
import {
|
||||||
@@ -118,6 +119,7 @@ export function AgentInputArea({
|
|||||||
const agentDirectoryStatus = useHostRuntimeAgentDirectoryStatus(serverId);
|
const agentDirectoryStatus = useHostRuntimeAgentDirectoryStatus(serverId);
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const voice = useVoiceOptional();
|
const voice = useVoiceOptional();
|
||||||
|
const voiceToggleKeys = useShortcutKeys("voice-toggle");
|
||||||
const isDictationReady =
|
const isDictationReady =
|
||||||
isConnected &&
|
isConnected &&
|
||||||
(agentDirectoryStatus === "ready" ||
|
(agentDirectoryStatus === "ready" ||
|
||||||
@@ -623,7 +625,7 @@ export function AgentInputArea({
|
|||||||
<TooltipContent side="top" align="center" offset={8}>
|
<TooltipContent side="top" align="center" offset={8}>
|
||||||
<View style={styles.tooltipRow}>
|
<View style={styles.tooltipRow}>
|
||||||
<Text style={styles.tooltipText}>Voice mode</Text>
|
<Text style={styles.tooltipText}>Voice mode</Text>
|
||||||
<Shortcut keys={["mod", "shift", "D"]} style={styles.tooltipShortcut} />
|
{voiceToggleKeys && <Shortcut keys={voiceToggleKeys} style={styles.tooltipShortcut} />}
|
||||||
</View>
|
</View>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyl
|
|||||||
import { MessagesSquare, Plus, Settings } from "lucide-react-native";
|
import { MessagesSquare, Plus, Settings } from "lucide-react-native";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
import { Shortcut } from "@/components/ui/shortcut";
|
import { Shortcut } from "@/components/ui/shortcut";
|
||||||
|
import { useShortcutKeys } from "@/hooks/use-shortcut-keys";
|
||||||
import { router, usePathname } from "expo-router";
|
import { router, usePathname } from "expo-router";
|
||||||
import { usePanelStore } from "@/stores/panel-store";
|
import { usePanelStore } from "@/stores/panel-store";
|
||||||
import { SidebarWorkspaceList } from "./sidebar-workspace-list";
|
import { SidebarWorkspaceList } from "./sidebar-workspace-list";
|
||||||
@@ -353,6 +354,7 @@ function MobileSidebar({
|
|||||||
closeToAgent,
|
closeToAgent,
|
||||||
handleViewMoreNavigate,
|
handleViewMoreNavigate,
|
||||||
}: MobileSidebarProps) {
|
}: MobileSidebarProps) {
|
||||||
|
const newAgentKeys = useShortcutKeys("new-agent");
|
||||||
const {
|
const {
|
||||||
translateX,
|
translateX,
|
||||||
backdropOpacity,
|
backdropOpacity,
|
||||||
@@ -562,7 +564,7 @@ function MobileSidebar({
|
|||||||
<TooltipContent side="top" align="center" offset={8}>
|
<TooltipContent side="top" align="center" offset={8}>
|
||||||
<View style={styles.tooltipRow}>
|
<View style={styles.tooltipRow}>
|
||||||
<Text style={styles.tooltipText}>Add project</Text>
|
<Text style={styles.tooltipText}>Add project</Text>
|
||||||
<Shortcut keys={["⌘", "⇧", "O"]} />
|
{newAgentKeys && <Shortcut keys={newAgentKeys} />}
|
||||||
</View>
|
</View>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -626,6 +628,7 @@ function DesktopSidebar({
|
|||||||
isOpen,
|
isOpen,
|
||||||
handleViewMore,
|
handleViewMore,
|
||||||
}: DesktopSidebarProps) {
|
}: DesktopSidebarProps) {
|
||||||
|
const newAgentKeys = useShortcutKeys("new-agent");
|
||||||
const dragHandlers = useDesktopDragHandlers();
|
const dragHandlers = useDesktopDragHandlers();
|
||||||
const trafficLightPadding = useTrafficLightPadding();
|
const trafficLightPadding = useTrafficLightPadding();
|
||||||
const hostStatusDotStyle = useMemo(
|
const hostStatusDotStyle = useMemo(
|
||||||
|
|||||||
@@ -13,6 +13,11 @@ import {
|
|||||||
} from "@/utils/command-center-focus-restore";
|
} from "@/utils/command-center-focus-restore";
|
||||||
import { buildHostSettingsRoute, parseServerIdFromPathname } from "@/utils/host-routes";
|
import { buildHostSettingsRoute, parseServerIdFromPathname } from "@/utils/host-routes";
|
||||||
import type { ShortcutKey } from "@/utils/format-shortcut";
|
import type { ShortcutKey } from "@/utils/format-shortcut";
|
||||||
|
import { comboStringToShortcutKeys } from "@/keyboard/shortcut-string";
|
||||||
|
import { getBindingIdForAction, getDefaultKeysForAction } from "@/keyboard/keyboard-shortcuts";
|
||||||
|
import { useKeyboardShortcutOverrides } from "@/hooks/use-keyboard-shortcut-overrides";
|
||||||
|
import { getShortcutOs } from "@/utils/shortcut-platform";
|
||||||
|
import { getIsDesktop } from "@/constants/layout";
|
||||||
import { prepareWorkspaceTab } from "@/utils/workspace-navigation";
|
import { prepareWorkspaceTab } from "@/utils/workspace-navigation";
|
||||||
import { focusWithRetries } from "@/utils/web-focus";
|
import { focusWithRetries } from "@/utils/web-focus";
|
||||||
|
|
||||||
@@ -48,7 +53,7 @@ type CommandCenterActionDefinition = {
|
|||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
icon?: "plus" | "settings";
|
icon?: "plus" | "settings";
|
||||||
shortcutKeys?: ShortcutKey[];
|
actionId?: string;
|
||||||
keywords: string[];
|
keywords: string[];
|
||||||
routeKind: "settings" | "none";
|
routeKind: "settings" | "none";
|
||||||
};
|
};
|
||||||
@@ -58,7 +63,7 @@ const COMMAND_CENTER_ACTIONS: readonly CommandCenterActionDefinition[] = [
|
|||||||
id: "new-agent",
|
id: "new-agent",
|
||||||
title: "Open project",
|
title: "Open project",
|
||||||
icon: "plus",
|
icon: "plus",
|
||||||
shortcutKeys: ["mod", "shift", "O"],
|
actionId: "new-agent",
|
||||||
keywords: ["open", "project", "folder", "workspace", "repo"],
|
keywords: ["open", "project", "folder", "workspace", "repo"],
|
||||||
routeKind: "none",
|
routeKind: "none",
|
||||||
},
|
},
|
||||||
@@ -99,9 +104,25 @@ export type CommandCenterItem =
|
|||||||
agent: AggregatedAgent;
|
agent: AggregatedAgent;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function resolveActionShortcutKeys(
|
||||||
|
actionId: string | undefined,
|
||||||
|
overrides: Record<string, string>,
|
||||||
|
): ShortcutKey[] | undefined {
|
||||||
|
if (!actionId) return undefined;
|
||||||
|
const isMac = getShortcutOs() === "mac";
|
||||||
|
const isDesktop = getIsDesktop();
|
||||||
|
const platform = { isMac, isDesktop };
|
||||||
|
const bindingId = getBindingIdForAction(actionId, platform);
|
||||||
|
if (!bindingId) return undefined;
|
||||||
|
const override = overrides[bindingId];
|
||||||
|
if (override) return comboStringToShortcutKeys(override);
|
||||||
|
return getDefaultKeysForAction(actionId, platform) ?? undefined;
|
||||||
|
}
|
||||||
|
|
||||||
export function useCommandCenter() {
|
export function useCommandCenter() {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const daemons = useHosts();
|
const daemons = useHosts();
|
||||||
|
const { overrides } = useKeyboardShortcutOverrides();
|
||||||
const open = useKeyboardShortcutsStore((s) => s.commandCenterOpen);
|
const open = useKeyboardShortcutsStore((s) => s.commandCenterOpen);
|
||||||
const setOpen = useKeyboardShortcutsStore((s) => s.setCommandCenterOpen);
|
const setOpen = useKeyboardShortcutsStore((s) => s.setCommandCenterOpen);
|
||||||
const inputRef = useRef<TextInput>(null);
|
const inputRef = useRef<TextInput>(null);
|
||||||
@@ -158,9 +179,9 @@ export function useCommandCenter() {
|
|||||||
title: action.title,
|
title: action.title,
|
||||||
icon: action.icon,
|
icon: action.icon,
|
||||||
route: action.routeKind === "settings" ? settingsRoute : undefined,
|
route: action.routeKind === "settings" ? settingsRoute : undefined,
|
||||||
shortcutKeys: action.shortcutKeys,
|
shortcutKeys: resolveActionShortcutKeys(action.actionId, overrides),
|
||||||
}));
|
}));
|
||||||
}, [open, query, settingsRoute]);
|
}, [open, query, settingsRoute, overrides]);
|
||||||
|
|
||||||
const items = useMemo(() => {
|
const items = useMemo(() => {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import { useCallback } from "react";
|
||||||
|
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||||
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
const STORAGE_KEY = "@paseo:keyboard-shortcut-overrides";
|
||||||
|
const QUERY_KEY = ["keyboard-shortcut-overrides"];
|
||||||
|
|
||||||
|
const EMPTY_OVERRIDES: Record<string, string> = {};
|
||||||
|
|
||||||
|
export interface UseKeyboardShortcutOverridesReturn {
|
||||||
|
overrides: Record<string, string>;
|
||||||
|
isLoading: boolean;
|
||||||
|
setOverride: (bindingId: string, comboString: string) => Promise<void>;
|
||||||
|
removeOverride: (bindingId: string) => Promise<void>;
|
||||||
|
resetAll: () => Promise<void>;
|
||||||
|
hasOverrides: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useKeyboardShortcutOverrides(): UseKeyboardShortcutOverridesReturn {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const { data, isPending } = useQuery({
|
||||||
|
queryKey: QUERY_KEY,
|
||||||
|
queryFn: loadOverridesFromStorage,
|
||||||
|
staleTime: Infinity,
|
||||||
|
gcTime: Infinity,
|
||||||
|
});
|
||||||
|
|
||||||
|
const setOverride = useCallback(
|
||||||
|
async (bindingId: string, comboString: string) => {
|
||||||
|
const prev =
|
||||||
|
queryClient.getQueryData<Record<string, string>>(QUERY_KEY) ?? EMPTY_OVERRIDES;
|
||||||
|
const next = { ...prev, [bindingId]: comboString };
|
||||||
|
queryClient.setQueryData<Record<string, string>>(QUERY_KEY, next);
|
||||||
|
await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(next));
|
||||||
|
},
|
||||||
|
[queryClient],
|
||||||
|
);
|
||||||
|
|
||||||
|
const removeOverride = useCallback(
|
||||||
|
async (bindingId: string) => {
|
||||||
|
const prev =
|
||||||
|
queryClient.getQueryData<Record<string, string>>(QUERY_KEY) ?? EMPTY_OVERRIDES;
|
||||||
|
const { [bindingId]: _, ...next } = prev;
|
||||||
|
queryClient.setQueryData<Record<string, string>>(QUERY_KEY, next);
|
||||||
|
await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(next));
|
||||||
|
},
|
||||||
|
[queryClient],
|
||||||
|
);
|
||||||
|
|
||||||
|
const resetAll = useCallback(async () => {
|
||||||
|
queryClient.setQueryData<Record<string, string>>(QUERY_KEY, EMPTY_OVERRIDES);
|
||||||
|
await AsyncStorage.removeItem(STORAGE_KEY);
|
||||||
|
}, [queryClient]);
|
||||||
|
|
||||||
|
const overrides = data ?? EMPTY_OVERRIDES;
|
||||||
|
|
||||||
|
return {
|
||||||
|
overrides,
|
||||||
|
isLoading: isPending,
|
||||||
|
setOverride,
|
||||||
|
removeOverride,
|
||||||
|
resetAll,
|
||||||
|
hasOverrides: Object.keys(overrides).length > 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadOverridesFromStorage(): Promise<Record<string, string>> {
|
||||||
|
try {
|
||||||
|
const stored = await AsyncStorage.getItem(STORAGE_KEY);
|
||||||
|
if (stored) {
|
||||||
|
return JSON.parse(stored) as Record<string, string>;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[KeyboardShortcutOverrides] Failed to load overrides:", err);
|
||||||
|
}
|
||||||
|
return EMPTY_OVERRIDES;
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect, useMemo } from "react";
|
||||||
import { Platform } from "react-native";
|
import { Platform } from "react-native";
|
||||||
import { usePathname } from "expo-router";
|
import { usePathname } from "expo-router";
|
||||||
import { getIsDesktop } from "@/constants/layout";
|
import { getIsDesktop } from "@/constants/layout";
|
||||||
@@ -17,10 +17,11 @@ import {
|
|||||||
} from "@/keyboard/actions";
|
} from "@/keyboard/actions";
|
||||||
import { canToggleFileExplorerShortcut } from "@/keyboard/keyboard-shortcut-routing";
|
import { canToggleFileExplorerShortcut } from "@/keyboard/keyboard-shortcut-routing";
|
||||||
import { keyboardActionDispatcher } from "@/keyboard/keyboard-action-dispatcher";
|
import { keyboardActionDispatcher } from "@/keyboard/keyboard-action-dispatcher";
|
||||||
import { resolveKeyboardShortcut } from "@/keyboard/keyboard-shortcuts";
|
import { resolveKeyboardShortcut, buildEffectiveBindings } from "@/keyboard/keyboard-shortcuts";
|
||||||
import { resolveKeyboardFocusScope } from "@/keyboard/focus-scope";
|
import { resolveKeyboardFocusScope } from "@/keyboard/focus-scope";
|
||||||
import { getShortcutOs } from "@/utils/shortcut-platform";
|
import { getShortcutOs } from "@/utils/shortcut-platform";
|
||||||
import { useOpenProjectPicker } from "@/hooks/use-open-project-picker";
|
import { useOpenProjectPicker } from "@/hooks/use-open-project-picker";
|
||||||
|
import { useKeyboardShortcutOverrides } from "@/hooks/use-keyboard-shortcut-overrides";
|
||||||
|
|
||||||
export function useKeyboardShortcuts({
|
export function useKeyboardShortcuts({
|
||||||
enabled,
|
enabled,
|
||||||
@@ -38,6 +39,8 @@ export function useKeyboardShortcuts({
|
|||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const hosts = useHosts();
|
const hosts = useHosts();
|
||||||
const resetModifiers = useKeyboardShortcutsStore((s) => s.resetModifiers);
|
const resetModifiers = useKeyboardShortcutsStore((s) => s.resetModifiers);
|
||||||
|
const { overrides } = useKeyboardShortcutOverrides();
|
||||||
|
const bindings = useMemo(() => buildEffectiveBindings(overrides), [overrides]);
|
||||||
const activeServerIdFromPath = parseServerIdFromPathname(pathname);
|
const activeServerIdFromPath = parseServerIdFromPathname(pathname);
|
||||||
const activeServerId =
|
const activeServerId =
|
||||||
hosts.find((host) => host.serverId === activeServerIdFromPath)?.serverId ??
|
hosts.find((host) => host.serverId === activeServerIdFromPath)?.serverId ??
|
||||||
@@ -285,6 +288,7 @@ export function useKeyboardShortcuts({
|
|||||||
toggleFileExplorer,
|
toggleFileExplorer,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
bindings,
|
||||||
});
|
});
|
||||||
if (!match) {
|
if (!match) {
|
||||||
return;
|
return;
|
||||||
@@ -332,6 +336,7 @@ export function useKeyboardShortcuts({
|
|||||||
document.removeEventListener("visibilitychange", handleBlurOrHide);
|
document.removeEventListener("visibilitychange", handleBlurOrHide);
|
||||||
};
|
};
|
||||||
}, [
|
}, [
|
||||||
|
bindings,
|
||||||
enabled,
|
enabled,
|
||||||
isMobile,
|
isMobile,
|
||||||
openProjectPickerAction,
|
openProjectPickerAction,
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
import type { ShortcutKey } from "@/utils/format-shortcut";
|
||||||
|
import { comboStringToShortcutKeys } from "@/keyboard/shortcut-string";
|
||||||
|
import { getBindingIdForAction, getDefaultKeysForAction } from "@/keyboard/keyboard-shortcuts";
|
||||||
|
import { useKeyboardShortcutOverrides } from "@/hooks/use-keyboard-shortcut-overrides";
|
||||||
|
import { getShortcutOs } from "@/utils/shortcut-platform";
|
||||||
|
import { getIsDesktop } from "@/constants/layout";
|
||||||
|
|
||||||
|
export function useShortcutKeys(actionId: string): ShortcutKey[] | null {
|
||||||
|
const { overrides } = useKeyboardShortcutOverrides();
|
||||||
|
const isMac = getShortcutOs() === "mac";
|
||||||
|
const isDesktop = getIsDesktop();
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
const platform = { isMac, isDesktop };
|
||||||
|
const bindingId = getBindingIdForAction(actionId, platform);
|
||||||
|
if (!bindingId) return null;
|
||||||
|
|
||||||
|
const override = overrides[bindingId];
|
||||||
|
if (override) {
|
||||||
|
return comboStringToShortcutKeys(override);
|
||||||
|
}
|
||||||
|
|
||||||
|
return getDefaultKeysForAction(actionId, platform);
|
||||||
|
}, [actionId, overrides, isMac, isDesktop]);
|
||||||
|
}
|
||||||
@@ -5,6 +5,9 @@ import type {
|
|||||||
KeyboardShortcutPayload,
|
KeyboardShortcutPayload,
|
||||||
MessageInputKeyboardActionKind,
|
MessageInputKeyboardActionKind,
|
||||||
} from "@/keyboard/actions";
|
} from "@/keyboard/actions";
|
||||||
|
import { type KeyCombo, parseShortcutString } from "@/keyboard/shortcut-string";
|
||||||
|
|
||||||
|
export type { KeyCombo } from "@/keyboard/shortcut-string";
|
||||||
|
|
||||||
// --- Public types ---
|
// --- Public types ---
|
||||||
|
|
||||||
@@ -43,18 +46,6 @@ type KeyboardShortcutPlatformContext = {
|
|||||||
isDesktop: boolean;
|
isDesktop: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface KeyCombo {
|
|
||||||
/** Event.code to match. Use "Digit" for any digit 1-9. */
|
|
||||||
code: string;
|
|
||||||
/** Optional event.key fallback (case-insensitive). */
|
|
||||||
key?: string;
|
|
||||||
meta?: true;
|
|
||||||
ctrl?: true;
|
|
||||||
alt?: true;
|
|
||||||
shift?: true;
|
|
||||||
/** Set to false to block key repeat events. */
|
|
||||||
repeat?: false;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ShortcutWhen {
|
interface ShortcutWhen {
|
||||||
/** true = mac only, false = non-mac only */
|
/** true = mac only, false = non-mac only */
|
||||||
@@ -87,7 +78,8 @@ interface ShortcutHelp {
|
|||||||
interface ShortcutBinding {
|
interface ShortcutBinding {
|
||||||
id: string;
|
id: string;
|
||||||
action: KeyboardActionId;
|
action: KeyboardActionId;
|
||||||
combo: KeyCombo;
|
combo: string;
|
||||||
|
repeat?: false;
|
||||||
when?: ShortcutWhen;
|
when?: ShortcutWhen;
|
||||||
payload?: ShortcutPayloadDef;
|
payload?: ShortcutPayloadDef;
|
||||||
preventDefault?: boolean;
|
preventDefault?: boolean;
|
||||||
@@ -95,6 +87,10 @@ interface ShortcutBinding {
|
|||||||
help?: ShortcutHelp;
|
help?: ShortcutHelp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ParsedShortcutBinding extends ShortcutBinding {
|
||||||
|
parsedCombo: KeyCombo;
|
||||||
|
}
|
||||||
|
|
||||||
// --- Constants ---
|
// --- Constants ---
|
||||||
|
|
||||||
const SHORTCUT_HELP_SECTION_TITLES: Record<KeyboardShortcutHelpSection["id"], string> = {
|
const SHORTCUT_HELP_SECTION_TITLES: Record<KeyboardShortcutHelpSection["id"], string> = {
|
||||||
@@ -109,7 +105,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
|
|||||||
{
|
{
|
||||||
id: "agent-new-cmd-shift-o-mac",
|
id: "agent-new-cmd-shift-o-mac",
|
||||||
action: "agent.new",
|
action: "agent.new",
|
||||||
combo: { code: "KeyO", key: "o", meta: true, shift: true },
|
combo: "Cmd+Shift+O",
|
||||||
when: { mac: true },
|
when: { mac: true },
|
||||||
help: {
|
help: {
|
||||||
id: "new-agent",
|
id: "new-agent",
|
||||||
@@ -121,7 +117,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
|
|||||||
{
|
{
|
||||||
id: "agent-new-ctrl-shift-o-non-mac",
|
id: "agent-new-ctrl-shift-o-non-mac",
|
||||||
action: "agent.new",
|
action: "agent.new",
|
||||||
combo: { code: "KeyO", key: "o", ctrl: true, shift: true },
|
combo: "Ctrl+Shift+O",
|
||||||
when: { mac: false, terminal: false },
|
when: { mac: false, terminal: false },
|
||||||
help: {
|
help: {
|
||||||
id: "new-agent",
|
id: "new-agent",
|
||||||
@@ -135,7 +131,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
|
|||||||
{
|
{
|
||||||
id: "workspace-tab-new-cmd-t-mac",
|
id: "workspace-tab-new-cmd-t-mac",
|
||||||
action: "workspace.tab.new",
|
action: "workspace.tab.new",
|
||||||
combo: { code: "KeyT", key: "t", meta: true },
|
combo: "Cmd+T",
|
||||||
when: { mac: true, commandCenter: false },
|
when: { mac: true, commandCenter: false },
|
||||||
help: {
|
help: {
|
||||||
id: "workspace-tab-new",
|
id: "workspace-tab-new",
|
||||||
@@ -147,7 +143,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
|
|||||||
{
|
{
|
||||||
id: "workspace-tab-new-ctrl-t-non-mac",
|
id: "workspace-tab-new-ctrl-t-non-mac",
|
||||||
action: "workspace.tab.new",
|
action: "workspace.tab.new",
|
||||||
combo: { code: "KeyT", key: "t", ctrl: true },
|
combo: "Ctrl+T",
|
||||||
when: { mac: false, commandCenter: false, terminal: false },
|
when: { mac: false, commandCenter: false, terminal: false },
|
||||||
help: {
|
help: {
|
||||||
id: "workspace-tab-new",
|
id: "workspace-tab-new",
|
||||||
@@ -159,7 +155,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
|
|||||||
{
|
{
|
||||||
id: "workspace-tab-close-current-cmd-w-mac",
|
id: "workspace-tab-close-current-cmd-w-mac",
|
||||||
action: "workspace.tab.close.current",
|
action: "workspace.tab.close.current",
|
||||||
combo: { code: "KeyW", key: "w", meta: true },
|
combo: "Cmd+W",
|
||||||
when: { mac: true, desktop: true, commandCenter: false },
|
when: { mac: true, desktop: true, commandCenter: false },
|
||||||
help: {
|
help: {
|
||||||
id: "workspace-tab-close-current",
|
id: "workspace-tab-close-current",
|
||||||
@@ -171,7 +167,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
|
|||||||
{
|
{
|
||||||
id: "workspace-tab-close-current-ctrl-w-non-mac",
|
id: "workspace-tab-close-current-ctrl-w-non-mac",
|
||||||
action: "workspace.tab.close.current",
|
action: "workspace.tab.close.current",
|
||||||
combo: { code: "KeyW", key: "w", ctrl: true },
|
combo: "Ctrl+W",
|
||||||
when: { mac: false, desktop: true, commandCenter: false, terminal: false },
|
when: { mac: false, desktop: true, commandCenter: false, terminal: false },
|
||||||
help: {
|
help: {
|
||||||
id: "workspace-tab-close-current",
|
id: "workspace-tab-close-current",
|
||||||
@@ -183,7 +179,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
|
|||||||
{
|
{
|
||||||
id: "workspace-tab-close-current-alt-shift-w-web",
|
id: "workspace-tab-close-current-alt-shift-w-web",
|
||||||
action: "workspace.tab.close.current",
|
action: "workspace.tab.close.current",
|
||||||
combo: { code: "KeyW", key: "w", alt: true, shift: true },
|
combo: "Alt+Shift+W",
|
||||||
when: { desktop: false, commandCenter: false },
|
when: { desktop: false, commandCenter: false },
|
||||||
help: {
|
help: {
|
||||||
id: "workspace-tab-close-current",
|
id: "workspace-tab-close-current",
|
||||||
@@ -197,7 +193,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
|
|||||||
{
|
{
|
||||||
id: "workspace-navigate-index-cmd-digit-mac",
|
id: "workspace-navigate-index-cmd-digit-mac",
|
||||||
action: "workspace.navigate.index",
|
action: "workspace.navigate.index",
|
||||||
combo: { code: "Digit", meta: true },
|
combo: "Cmd+Digit",
|
||||||
when: { mac: true, desktop: true, commandCenter: false },
|
when: { mac: true, desktop: true, commandCenter: false },
|
||||||
payload: { type: "index" },
|
payload: { type: "index" },
|
||||||
help: {
|
help: {
|
||||||
@@ -210,7 +206,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
|
|||||||
{
|
{
|
||||||
id: "workspace-navigate-index-ctrl-digit-non-mac",
|
id: "workspace-navigate-index-ctrl-digit-non-mac",
|
||||||
action: "workspace.navigate.index",
|
action: "workspace.navigate.index",
|
||||||
combo: { code: "Digit", ctrl: true },
|
combo: "Ctrl+Digit",
|
||||||
when: { mac: false, desktop: true, commandCenter: false, terminal: false },
|
when: { mac: false, desktop: true, commandCenter: false, terminal: false },
|
||||||
payload: { type: "index" },
|
payload: { type: "index" },
|
||||||
help: {
|
help: {
|
||||||
@@ -223,7 +219,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
|
|||||||
{
|
{
|
||||||
id: "workspace-navigate-index-alt-digit-web",
|
id: "workspace-navigate-index-alt-digit-web",
|
||||||
action: "workspace.navigate.index",
|
action: "workspace.navigate.index",
|
||||||
combo: { code: "Digit", alt: true },
|
combo: "Alt+Digit",
|
||||||
when: { desktop: false, commandCenter: false },
|
when: { desktop: false, commandCenter: false },
|
||||||
payload: { type: "index" },
|
payload: { type: "index" },
|
||||||
help: {
|
help: {
|
||||||
@@ -238,7 +234,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
|
|||||||
{
|
{
|
||||||
id: "workspace-tab-navigate-index-alt-digit-desktop",
|
id: "workspace-tab-navigate-index-alt-digit-desktop",
|
||||||
action: "workspace.tab.navigate.index",
|
action: "workspace.tab.navigate.index",
|
||||||
combo: { code: "Digit", alt: true },
|
combo: "Alt+Digit",
|
||||||
when: { desktop: true, commandCenter: false },
|
when: { desktop: true, commandCenter: false },
|
||||||
payload: { type: "index" },
|
payload: { type: "index" },
|
||||||
help: {
|
help: {
|
||||||
@@ -251,7 +247,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
|
|||||||
{
|
{
|
||||||
id: "workspace-tab-navigate-index-alt-shift-digit-web",
|
id: "workspace-tab-navigate-index-alt-shift-digit-web",
|
||||||
action: "workspace.tab.navigate.index",
|
action: "workspace.tab.navigate.index",
|
||||||
combo: { code: "Digit", alt: true, shift: true },
|
combo: "Alt+Shift+Digit",
|
||||||
when: { desktop: false, commandCenter: false },
|
when: { desktop: false, commandCenter: false },
|
||||||
payload: { type: "index" },
|
payload: { type: "index" },
|
||||||
help: {
|
help: {
|
||||||
@@ -266,7 +262,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
|
|||||||
{
|
{
|
||||||
id: "workspace-navigate-relative-cmd-left-mac",
|
id: "workspace-navigate-relative-cmd-left-mac",
|
||||||
action: "workspace.navigate.relative",
|
action: "workspace.navigate.relative",
|
||||||
combo: { code: "BracketLeft", key: "[", meta: true },
|
combo: "Cmd+[",
|
||||||
when: { mac: true, desktop: true, commandCenter: false },
|
when: { mac: true, desktop: true, commandCenter: false },
|
||||||
payload: { type: "delta", delta: -1 },
|
payload: { type: "delta", delta: -1 },
|
||||||
help: {
|
help: {
|
||||||
@@ -279,7 +275,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
|
|||||||
{
|
{
|
||||||
id: "workspace-navigate-relative-ctrl-left-non-mac",
|
id: "workspace-navigate-relative-ctrl-left-non-mac",
|
||||||
action: "workspace.navigate.relative",
|
action: "workspace.navigate.relative",
|
||||||
combo: { code: "BracketLeft", key: "[", ctrl: true },
|
combo: "Ctrl+[",
|
||||||
when: { mac: false, desktop: true, commandCenter: false, terminal: false },
|
when: { mac: false, desktop: true, commandCenter: false, terminal: false },
|
||||||
payload: { type: "delta", delta: -1 },
|
payload: { type: "delta", delta: -1 },
|
||||||
help: {
|
help: {
|
||||||
@@ -292,7 +288,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
|
|||||||
{
|
{
|
||||||
id: "workspace-navigate-relative-cmd-right-mac",
|
id: "workspace-navigate-relative-cmd-right-mac",
|
||||||
action: "workspace.navigate.relative",
|
action: "workspace.navigate.relative",
|
||||||
combo: { code: "BracketRight", key: "]", meta: true },
|
combo: "Cmd+]",
|
||||||
when: { mac: true, desktop: true, commandCenter: false },
|
when: { mac: true, desktop: true, commandCenter: false },
|
||||||
payload: { type: "delta", delta: 1 },
|
payload: { type: "delta", delta: 1 },
|
||||||
help: {
|
help: {
|
||||||
@@ -305,7 +301,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
|
|||||||
{
|
{
|
||||||
id: "workspace-navigate-relative-ctrl-right-non-mac",
|
id: "workspace-navigate-relative-ctrl-right-non-mac",
|
||||||
action: "workspace.navigate.relative",
|
action: "workspace.navigate.relative",
|
||||||
combo: { code: "BracketRight", key: "]", ctrl: true },
|
combo: "Ctrl+]",
|
||||||
when: { mac: false, desktop: true, commandCenter: false, terminal: false },
|
when: { mac: false, desktop: true, commandCenter: false, terminal: false },
|
||||||
payload: { type: "delta", delta: 1 },
|
payload: { type: "delta", delta: 1 },
|
||||||
help: {
|
help: {
|
||||||
@@ -318,7 +314,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
|
|||||||
{
|
{
|
||||||
id: "workspace-navigate-relative-alt-left-web",
|
id: "workspace-navigate-relative-alt-left-web",
|
||||||
action: "workspace.navigate.relative",
|
action: "workspace.navigate.relative",
|
||||||
combo: { code: "BracketLeft", key: "[", alt: true },
|
combo: "Alt+[",
|
||||||
when: { desktop: false, commandCenter: false },
|
when: { desktop: false, commandCenter: false },
|
||||||
payload: { type: "delta", delta: -1 },
|
payload: { type: "delta", delta: -1 },
|
||||||
help: {
|
help: {
|
||||||
@@ -331,7 +327,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
|
|||||||
{
|
{
|
||||||
id: "workspace-navigate-relative-alt-right-web",
|
id: "workspace-navigate-relative-alt-right-web",
|
||||||
action: "workspace.navigate.relative",
|
action: "workspace.navigate.relative",
|
||||||
combo: { code: "BracketRight", key: "]", alt: true },
|
combo: "Alt+]",
|
||||||
when: { desktop: false, commandCenter: false },
|
when: { desktop: false, commandCenter: false },
|
||||||
payload: { type: "delta", delta: 1 },
|
payload: { type: "delta", delta: 1 },
|
||||||
help: {
|
help: {
|
||||||
@@ -346,7 +342,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
|
|||||||
{
|
{
|
||||||
id: "workspace-tab-navigate-relative-alt-shift-left",
|
id: "workspace-tab-navigate-relative-alt-shift-left",
|
||||||
action: "workspace.tab.navigate.relative",
|
action: "workspace.tab.navigate.relative",
|
||||||
combo: { code: "BracketLeft", alt: true, shift: true },
|
combo: "Alt+Shift+[",
|
||||||
when: { commandCenter: false },
|
when: { commandCenter: false },
|
||||||
payload: { type: "delta", delta: -1 },
|
payload: { type: "delta", delta: -1 },
|
||||||
help: {
|
help: {
|
||||||
@@ -359,7 +355,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
|
|||||||
{
|
{
|
||||||
id: "workspace-tab-navigate-relative-alt-shift-right",
|
id: "workspace-tab-navigate-relative-alt-shift-right",
|
||||||
action: "workspace.tab.navigate.relative",
|
action: "workspace.tab.navigate.relative",
|
||||||
combo: { code: "BracketRight", alt: true, shift: true },
|
combo: "Alt+Shift+]",
|
||||||
when: { commandCenter: false },
|
when: { commandCenter: false },
|
||||||
payload: { type: "delta", delta: 1 },
|
payload: { type: "delta", delta: 1 },
|
||||||
help: {
|
help: {
|
||||||
@@ -374,7 +370,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
|
|||||||
{
|
{
|
||||||
id: "workspace-pane-split-right-cmd-backslash",
|
id: "workspace-pane-split-right-cmd-backslash",
|
||||||
action: "workspace.pane.split.right",
|
action: "workspace.pane.split.right",
|
||||||
combo: { code: "Backslash", meta: true },
|
combo: "Cmd+\\",
|
||||||
when: { mac: true, terminal: false, commandCenter: false },
|
when: { mac: true, terminal: false, commandCenter: false },
|
||||||
help: {
|
help: {
|
||||||
id: "workspace-pane-split-right",
|
id: "workspace-pane-split-right",
|
||||||
@@ -386,7 +382,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
|
|||||||
{
|
{
|
||||||
id: "workspace-pane-split-down-cmd-shift-backslash",
|
id: "workspace-pane-split-down-cmd-shift-backslash",
|
||||||
action: "workspace.pane.split.down",
|
action: "workspace.pane.split.down",
|
||||||
combo: { code: "Backslash", meta: true, shift: true },
|
combo: "Cmd+Shift+\\",
|
||||||
when: { mac: true, terminal: false, commandCenter: false },
|
when: { mac: true, terminal: false, commandCenter: false },
|
||||||
help: {
|
help: {
|
||||||
id: "workspace-pane-split-down",
|
id: "workspace-pane-split-down",
|
||||||
@@ -398,7 +394,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
|
|||||||
{
|
{
|
||||||
id: "workspace-pane-focus-left-cmd-shift-left",
|
id: "workspace-pane-focus-left-cmd-shift-left",
|
||||||
action: "workspace.pane.focus.left",
|
action: "workspace.pane.focus.left",
|
||||||
combo: { code: "ArrowLeft", meta: true, shift: true },
|
combo: "Cmd+Shift+ArrowLeft",
|
||||||
when: { mac: true, terminal: false, commandCenter: false },
|
when: { mac: true, terminal: false, commandCenter: false },
|
||||||
help: {
|
help: {
|
||||||
id: "workspace-pane-focus-left",
|
id: "workspace-pane-focus-left",
|
||||||
@@ -410,7 +406,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
|
|||||||
{
|
{
|
||||||
id: "workspace-pane-focus-right-cmd-shift-right",
|
id: "workspace-pane-focus-right-cmd-shift-right",
|
||||||
action: "workspace.pane.focus.right",
|
action: "workspace.pane.focus.right",
|
||||||
combo: { code: "ArrowRight", meta: true, shift: true },
|
combo: "Cmd+Shift+ArrowRight",
|
||||||
when: { mac: true, terminal: false, commandCenter: false },
|
when: { mac: true, terminal: false, commandCenter: false },
|
||||||
help: {
|
help: {
|
||||||
id: "workspace-pane-focus-right",
|
id: "workspace-pane-focus-right",
|
||||||
@@ -422,7 +418,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
|
|||||||
{
|
{
|
||||||
id: "workspace-pane-focus-up-cmd-shift-up",
|
id: "workspace-pane-focus-up-cmd-shift-up",
|
||||||
action: "workspace.pane.focus.up",
|
action: "workspace.pane.focus.up",
|
||||||
combo: { code: "ArrowUp", meta: true, shift: true },
|
combo: "Cmd+Shift+ArrowUp",
|
||||||
when: { mac: true, terminal: false, commandCenter: false },
|
when: { mac: true, terminal: false, commandCenter: false },
|
||||||
help: {
|
help: {
|
||||||
id: "workspace-pane-focus-up",
|
id: "workspace-pane-focus-up",
|
||||||
@@ -434,7 +430,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
|
|||||||
{
|
{
|
||||||
id: "workspace-pane-focus-down-cmd-shift-down",
|
id: "workspace-pane-focus-down-cmd-shift-down",
|
||||||
action: "workspace.pane.focus.down",
|
action: "workspace.pane.focus.down",
|
||||||
combo: { code: "ArrowDown", meta: true, shift: true },
|
combo: "Cmd+Shift+ArrowDown",
|
||||||
when: { mac: true, terminal: false, commandCenter: false },
|
when: { mac: true, terminal: false, commandCenter: false },
|
||||||
help: {
|
help: {
|
||||||
id: "workspace-pane-focus-down",
|
id: "workspace-pane-focus-down",
|
||||||
@@ -446,7 +442,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
|
|||||||
{
|
{
|
||||||
id: "workspace-pane-move-tab-left-cmd-shift-alt-left",
|
id: "workspace-pane-move-tab-left-cmd-shift-alt-left",
|
||||||
action: "workspace.pane.move-tab.left",
|
action: "workspace.pane.move-tab.left",
|
||||||
combo: { code: "ArrowLeft", meta: true, alt: true, shift: true },
|
combo: "Cmd+Alt+Shift+ArrowLeft",
|
||||||
when: { mac: true, terminal: false, commandCenter: false },
|
when: { mac: true, terminal: false, commandCenter: false },
|
||||||
help: {
|
help: {
|
||||||
id: "workspace-pane-move-tab-left",
|
id: "workspace-pane-move-tab-left",
|
||||||
@@ -458,7 +454,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
|
|||||||
{
|
{
|
||||||
id: "workspace-pane-move-tab-right-cmd-shift-alt-right",
|
id: "workspace-pane-move-tab-right-cmd-shift-alt-right",
|
||||||
action: "workspace.pane.move-tab.right",
|
action: "workspace.pane.move-tab.right",
|
||||||
combo: { code: "ArrowRight", meta: true, alt: true, shift: true },
|
combo: "Cmd+Alt+Shift+ArrowRight",
|
||||||
when: { mac: true, terminal: false, commandCenter: false },
|
when: { mac: true, terminal: false, commandCenter: false },
|
||||||
help: {
|
help: {
|
||||||
id: "workspace-pane-move-tab-right",
|
id: "workspace-pane-move-tab-right",
|
||||||
@@ -470,7 +466,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
|
|||||||
{
|
{
|
||||||
id: "workspace-pane-move-tab-up-cmd-shift-alt-up",
|
id: "workspace-pane-move-tab-up-cmd-shift-alt-up",
|
||||||
action: "workspace.pane.move-tab.up",
|
action: "workspace.pane.move-tab.up",
|
||||||
combo: { code: "ArrowUp", meta: true, alt: true, shift: true },
|
combo: "Cmd+Alt+Shift+ArrowUp",
|
||||||
when: { mac: true, terminal: false, commandCenter: false },
|
when: { mac: true, terminal: false, commandCenter: false },
|
||||||
help: {
|
help: {
|
||||||
id: "workspace-pane-move-tab-up",
|
id: "workspace-pane-move-tab-up",
|
||||||
@@ -482,7 +478,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
|
|||||||
{
|
{
|
||||||
id: "workspace-pane-move-tab-down-cmd-shift-alt-down",
|
id: "workspace-pane-move-tab-down-cmd-shift-alt-down",
|
||||||
action: "workspace.pane.move-tab.down",
|
action: "workspace.pane.move-tab.down",
|
||||||
combo: { code: "ArrowDown", meta: true, alt: true, shift: true },
|
combo: "Cmd+Alt+Shift+ArrowDown",
|
||||||
when: { mac: true, terminal: false, commandCenter: false },
|
when: { mac: true, terminal: false, commandCenter: false },
|
||||||
help: {
|
help: {
|
||||||
id: "workspace-pane-move-tab-down",
|
id: "workspace-pane-move-tab-down",
|
||||||
@@ -494,7 +490,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
|
|||||||
{
|
{
|
||||||
id: "workspace-pane-close-cmd-shift-w",
|
id: "workspace-pane-close-cmd-shift-w",
|
||||||
action: "workspace.pane.close",
|
action: "workspace.pane.close",
|
||||||
combo: { code: "KeyW", key: "w", meta: true, shift: true },
|
combo: "Cmd+Shift+W",
|
||||||
when: { mac: true, terminal: false, commandCenter: false },
|
when: { mac: true, terminal: false, commandCenter: false },
|
||||||
help: {
|
help: {
|
||||||
id: "workspace-pane-close",
|
id: "workspace-pane-close",
|
||||||
@@ -508,7 +504,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
|
|||||||
{
|
{
|
||||||
id: "command-center-toggle-cmd-k-mac",
|
id: "command-center-toggle-cmd-k-mac",
|
||||||
action: "command-center.toggle",
|
action: "command-center.toggle",
|
||||||
combo: { code: "KeyK", key: "k", meta: true },
|
combo: "Cmd+K",
|
||||||
when: { mac: true },
|
when: { mac: true },
|
||||||
help: {
|
help: {
|
||||||
id: "toggle-command-center",
|
id: "toggle-command-center",
|
||||||
@@ -520,7 +516,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
|
|||||||
{
|
{
|
||||||
id: "command-center-toggle-ctrl-k-non-mac",
|
id: "command-center-toggle-ctrl-k-non-mac",
|
||||||
action: "command-center.toggle",
|
action: "command-center.toggle",
|
||||||
combo: { code: "KeyK", key: "k", ctrl: true },
|
combo: "Ctrl+K",
|
||||||
when: { mac: false, terminal: false },
|
when: { mac: false, terminal: false },
|
||||||
help: {
|
help: {
|
||||||
id: "toggle-command-center",
|
id: "toggle-command-center",
|
||||||
@@ -534,7 +530,8 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
|
|||||||
{
|
{
|
||||||
id: "shortcuts-dialog-toggle-question-mark",
|
id: "shortcuts-dialog-toggle-question-mark",
|
||||||
action: "shortcuts.dialog.toggle",
|
action: "shortcuts.dialog.toggle",
|
||||||
combo: { code: "Slash", key: "?", shift: true, repeat: false },
|
combo: "Shift+?",
|
||||||
|
repeat: false,
|
||||||
when: { focusScope: "other" },
|
when: { focusScope: "other" },
|
||||||
help: {
|
help: {
|
||||||
id: "show-shortcuts",
|
id: "show-shortcuts",
|
||||||
@@ -549,7 +546,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
|
|||||||
{
|
{
|
||||||
id: "sidebar-toggle-left-mac-cmd-b",
|
id: "sidebar-toggle-left-mac-cmd-b",
|
||||||
action: "sidebar.toggle.left",
|
action: "sidebar.toggle.left",
|
||||||
combo: { code: "KeyB", key: "b", meta: true },
|
combo: "Cmd+B",
|
||||||
when: { mac: true },
|
when: { mac: true },
|
||||||
help: {
|
help: {
|
||||||
id: "toggle-left-sidebar",
|
id: "toggle-left-sidebar",
|
||||||
@@ -561,13 +558,13 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
|
|||||||
{
|
{
|
||||||
id: "sidebar-toggle-left-cmd-period-mac",
|
id: "sidebar-toggle-left-cmd-period-mac",
|
||||||
action: "sidebar.toggle.left",
|
action: "sidebar.toggle.left",
|
||||||
combo: { code: "Period", key: ".", meta: true },
|
combo: "Cmd+.",
|
||||||
when: { mac: true, commandCenter: false },
|
when: { mac: true, commandCenter: false },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "sidebar-toggle-left-ctrl-period-non-mac",
|
id: "sidebar-toggle-left-ctrl-period-non-mac",
|
||||||
action: "sidebar.toggle.left",
|
action: "sidebar.toggle.left",
|
||||||
combo: { code: "Period", key: ".", ctrl: true },
|
combo: "Ctrl+.",
|
||||||
when: { mac: false, commandCenter: false, terminal: false },
|
when: { mac: false, commandCenter: false, terminal: false },
|
||||||
help: {
|
help: {
|
||||||
id: "toggle-left-sidebar",
|
id: "toggle-left-sidebar",
|
||||||
@@ -579,7 +576,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
|
|||||||
{
|
{
|
||||||
id: "sidebar-toggle-right-cmd-e-mac",
|
id: "sidebar-toggle-right-cmd-e-mac",
|
||||||
action: "sidebar.toggle.right",
|
action: "sidebar.toggle.right",
|
||||||
combo: { code: "KeyE", key: "e", meta: true },
|
combo: "Cmd+E",
|
||||||
when: { mac: true, hasSelectedAgent: true, commandCenter: false },
|
when: { mac: true, hasSelectedAgent: true, commandCenter: false },
|
||||||
help: {
|
help: {
|
||||||
id: "toggle-right-sidebar",
|
id: "toggle-right-sidebar",
|
||||||
@@ -591,7 +588,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
|
|||||||
{
|
{
|
||||||
id: "sidebar-toggle-right-ctrl-e-non-mac",
|
id: "sidebar-toggle-right-ctrl-e-non-mac",
|
||||||
action: "sidebar.toggle.right",
|
action: "sidebar.toggle.right",
|
||||||
combo: { code: "KeyE", key: "e", ctrl: true },
|
combo: "Ctrl+E",
|
||||||
when: { mac: false, hasSelectedAgent: true, commandCenter: false, terminal: false },
|
when: { mac: false, hasSelectedAgent: true, commandCenter: false, terminal: false },
|
||||||
help: {
|
help: {
|
||||||
id: "toggle-right-sidebar",
|
id: "toggle-right-sidebar",
|
||||||
@@ -603,7 +600,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
|
|||||||
{
|
{
|
||||||
id: "sidebar-toggle-right-ctrl-backquote",
|
id: "sidebar-toggle-right-ctrl-backquote",
|
||||||
action: "sidebar.toggle.right",
|
action: "sidebar.toggle.right",
|
||||||
combo: { code: "Backquote", key: "`", ctrl: true },
|
combo: "Ctrl+`",
|
||||||
when: { hasSelectedAgent: true, commandCenter: false },
|
when: { hasSelectedAgent: true, commandCenter: false },
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -611,7 +608,8 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
|
|||||||
{
|
{
|
||||||
id: "message-input-voice-toggle-cmd-shift-d-mac",
|
id: "message-input-voice-toggle-cmd-shift-d-mac",
|
||||||
action: "message-input.action",
|
action: "message-input.action",
|
||||||
combo: { code: "KeyD", key: "d", meta: true, shift: true, repeat: false },
|
combo: "Cmd+Shift+D",
|
||||||
|
repeat: false,
|
||||||
when: { mac: true, commandCenter: false, terminal: false },
|
when: { mac: true, commandCenter: false, terminal: false },
|
||||||
payload: { type: "message-input", kind: "voice-toggle" },
|
payload: { type: "message-input", kind: "voice-toggle" },
|
||||||
help: {
|
help: {
|
||||||
@@ -624,7 +622,8 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
|
|||||||
{
|
{
|
||||||
id: "message-input-voice-toggle-ctrl-shift-d-non-mac",
|
id: "message-input-voice-toggle-ctrl-shift-d-non-mac",
|
||||||
action: "message-input.action",
|
action: "message-input.action",
|
||||||
combo: { code: "KeyD", key: "d", ctrl: true, shift: true, repeat: false },
|
combo: "Ctrl+Shift+D",
|
||||||
|
repeat: false,
|
||||||
when: { mac: false, commandCenter: false, terminal: false },
|
when: { mac: false, commandCenter: false, terminal: false },
|
||||||
payload: { type: "message-input", kind: "voice-toggle" },
|
payload: { type: "message-input", kind: "voice-toggle" },
|
||||||
help: {
|
help: {
|
||||||
@@ -637,7 +636,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
|
|||||||
{
|
{
|
||||||
id: "message-input-dictation-toggle-cmd-d-mac",
|
id: "message-input-dictation-toggle-cmd-d-mac",
|
||||||
action: "message-input.action",
|
action: "message-input.action",
|
||||||
combo: { code: "KeyD", key: "d", meta: true },
|
combo: "Cmd+D",
|
||||||
when: { mac: true, commandCenter: false, terminal: false },
|
when: { mac: true, commandCenter: false, terminal: false },
|
||||||
payload: { type: "message-input", kind: "dictation-toggle" },
|
payload: { type: "message-input", kind: "dictation-toggle" },
|
||||||
help: {
|
help: {
|
||||||
@@ -650,7 +649,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
|
|||||||
{
|
{
|
||||||
id: "message-input-dictation-toggle-ctrl-d-non-mac",
|
id: "message-input-dictation-toggle-ctrl-d-non-mac",
|
||||||
action: "message-input.action",
|
action: "message-input.action",
|
||||||
combo: { code: "KeyD", key: "d", ctrl: true },
|
combo: "Ctrl+D",
|
||||||
when: { mac: false, commandCenter: false, terminal: false },
|
when: { mac: false, commandCenter: false, terminal: false },
|
||||||
payload: { type: "message-input", kind: "dictation-toggle" },
|
payload: { type: "message-input", kind: "dictation-toggle" },
|
||||||
help: {
|
help: {
|
||||||
@@ -663,7 +662,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
|
|||||||
{
|
{
|
||||||
id: "message-input-dictation-cancel",
|
id: "message-input-dictation-cancel",
|
||||||
action: "message-input.action",
|
action: "message-input.action",
|
||||||
combo: { code: "Escape" },
|
combo: "Escape",
|
||||||
when: { commandCenter: false, terminal: false },
|
when: { commandCenter: false, terminal: false },
|
||||||
payload: { type: "message-input", kind: "dictation-cancel" },
|
payload: { type: "message-input", kind: "dictation-cancel" },
|
||||||
preventDefault: false,
|
preventDefault: false,
|
||||||
@@ -678,7 +677,8 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
|
|||||||
{
|
{
|
||||||
id: "message-input-voice-mute-toggle",
|
id: "message-input-voice-mute-toggle",
|
||||||
action: "message-input.action",
|
action: "message-input.action",
|
||||||
combo: { code: "Space", key: " ", repeat: false },
|
combo: "Space",
|
||||||
|
repeat: false,
|
||||||
when: { commandCenter: false, focusScope: "other" },
|
when: { commandCenter: false, focusScope: "other" },
|
||||||
payload: { type: "message-input", kind: "voice-mute-toggle" },
|
payload: { type: "message-input", kind: "voice-mute-toggle" },
|
||||||
help: {
|
help: {
|
||||||
@@ -690,6 +690,40 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// --- Parse bindings at module load ---
|
||||||
|
|
||||||
|
function parseBinding(binding: ShortcutBinding): ParsedShortcutBinding {
|
||||||
|
const parsedCombo = parseShortcutString(binding.combo);
|
||||||
|
if (binding.repeat === false) {
|
||||||
|
parsedCombo.repeat = false;
|
||||||
|
}
|
||||||
|
return { ...binding, parsedCombo };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_BINDINGS: readonly ParsedShortcutBinding[] =
|
||||||
|
SHORTCUT_BINDINGS.map(parseBinding);
|
||||||
|
|
||||||
|
export function buildEffectiveBindings(
|
||||||
|
overrides: Record<string, string>,
|
||||||
|
): ParsedShortcutBinding[] {
|
||||||
|
return DEFAULT_BINDINGS.map(function (binding) {
|
||||||
|
const override = overrides[binding.id];
|
||||||
|
if (override === undefined) {
|
||||||
|
return binding;
|
||||||
|
}
|
||||||
|
let parsedCombo: KeyCombo;
|
||||||
|
try {
|
||||||
|
parsedCombo = parseShortcutString(override);
|
||||||
|
} catch {
|
||||||
|
return binding;
|
||||||
|
}
|
||||||
|
if (binding.repeat === false) {
|
||||||
|
parsedCombo.repeat = false;
|
||||||
|
}
|
||||||
|
return { ...binding, combo: override, parsedCombo };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// --- Matching engine ---
|
// --- Matching engine ---
|
||||||
|
|
||||||
function parseDigit(event: KeyboardEvent): number | null {
|
function parseDigit(event: KeyboardEvent): number | null {
|
||||||
@@ -709,9 +743,19 @@ function parseDigit(event: KeyboardEvent): number | null {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function matchesCombo(combo: KeyCombo, event: KeyboardEvent): boolean {
|
function matchesCombo(combo: KeyCombo, event: KeyboardEvent, isMac: boolean): boolean {
|
||||||
if (!!combo.meta !== event.metaKey) return false;
|
if (combo.mod) {
|
||||||
if (!!combo.ctrl !== event.ctrlKey) return false;
|
if (isMac) {
|
||||||
|
if (!event.metaKey) return false;
|
||||||
|
if (!!combo.ctrl !== event.ctrlKey) return false;
|
||||||
|
} else {
|
||||||
|
if (!event.ctrlKey) return false;
|
||||||
|
if (!!combo.meta !== event.metaKey) return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!!combo.meta !== event.metaKey) return false;
|
||||||
|
if (!!combo.ctrl !== event.ctrlKey) return false;
|
||||||
|
}
|
||||||
if (!!combo.alt !== event.altKey) return false;
|
if (!!combo.alt !== event.altKey) return false;
|
||||||
if (!!combo.shift !== event.shiftKey) return false;
|
if (!!combo.shift !== event.shiftKey) return false;
|
||||||
if (combo.repeat === false && event.repeat) return false;
|
if (combo.repeat === false && event.repeat) return false;
|
||||||
@@ -767,10 +811,11 @@ function helpMatchesPlatform(
|
|||||||
export function resolveKeyboardShortcut(input: {
|
export function resolveKeyboardShortcut(input: {
|
||||||
event: KeyboardEvent;
|
event: KeyboardEvent;
|
||||||
context: KeyboardShortcutContext;
|
context: KeyboardShortcutContext;
|
||||||
|
bindings?: readonly ParsedShortcutBinding[];
|
||||||
}): KeyboardShortcutMatch | null {
|
}): KeyboardShortcutMatch | null {
|
||||||
const { event, context } = input;
|
const { event, context, bindings = DEFAULT_BINDINGS } = input;
|
||||||
for (const binding of SHORTCUT_BINDINGS) {
|
for (const binding of bindings) {
|
||||||
if (!matchesCombo(binding.combo, event)) {
|
if (!matchesCombo(binding.parsedCombo, event, context.isMac)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!matchesWhen(binding.when, context)) {
|
if (!matchesWhen(binding.when, context)) {
|
||||||
@@ -786,8 +831,41 @@ export function resolveKeyboardShortcut(input: {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getBindingIdForAction(
|
||||||
|
actionId: string,
|
||||||
|
platform: { isMac: boolean; isDesktop: boolean },
|
||||||
|
): string | null {
|
||||||
|
for (const binding of DEFAULT_BINDINGS) {
|
||||||
|
if (binding.help?.id !== actionId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!helpMatchesPlatform(binding.when, platform)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return binding.id;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDefaultKeysForAction(
|
||||||
|
actionId: string,
|
||||||
|
platform: { isMac: boolean; isDesktop: boolean },
|
||||||
|
): ShortcutKey[] | null {
|
||||||
|
for (const binding of DEFAULT_BINDINGS) {
|
||||||
|
if (binding.help?.id !== actionId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!helpMatchesPlatform(binding.when, platform)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return binding.help.keys;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
export function buildKeyboardShortcutHelpSections(
|
export function buildKeyboardShortcutHelpSections(
|
||||||
input: KeyboardShortcutPlatformContext,
|
input: KeyboardShortcutPlatformContext,
|
||||||
|
bindings: readonly ParsedShortcutBinding[] = DEFAULT_BINDINGS,
|
||||||
): KeyboardShortcutHelpSection[] {
|
): KeyboardShortcutHelpSection[] {
|
||||||
const seenRows = new Set<string>();
|
const seenRows = new Set<string>();
|
||||||
const rowsBySection = new Map<KeyboardShortcutHelpSection["id"], KeyboardShortcutHelpRow[]>([
|
const rowsBySection = new Map<KeyboardShortcutHelpSection["id"], KeyboardShortcutHelpRow[]>([
|
||||||
@@ -795,7 +873,7 @@ export function buildKeyboardShortcutHelpSections(
|
|||||||
["agent-input", []],
|
["agent-input", []],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
for (const binding of SHORTCUT_BINDINGS) {
|
for (const binding of bindings) {
|
||||||
const help = binding.help;
|
const help = binding.help;
|
||||||
if (!help) {
|
if (!help) {
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -0,0 +1,179 @@
|
|||||||
|
import type { ShortcutKey } from "@/utils/format-shortcut";
|
||||||
|
|
||||||
|
export interface KeyCombo {
|
||||||
|
code: string;
|
||||||
|
key?: string;
|
||||||
|
meta?: true;
|
||||||
|
ctrl?: true;
|
||||||
|
alt?: true;
|
||||||
|
shift?: true;
|
||||||
|
mod?: true;
|
||||||
|
repeat?: false;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface KeyMapping {
|
||||||
|
code: string;
|
||||||
|
key?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const KEY_MAP: Record<string, KeyMapping> = {};
|
||||||
|
|
||||||
|
for (let i = 0; i < 26; i++) {
|
||||||
|
const letter = String.fromCharCode(65 + i);
|
||||||
|
KEY_MAP[letter] = { code: `Key${letter}`, key: letter.toLowerCase() };
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i <= 9; i++) {
|
||||||
|
KEY_MAP[String(i)] = { code: `Digit${i}`, key: String(i) };
|
||||||
|
}
|
||||||
|
|
||||||
|
KEY_MAP["Digit"] = { code: "Digit" };
|
||||||
|
KEY_MAP["\\"] = { code: "Backslash" };
|
||||||
|
KEY_MAP["["] = { code: "BracketLeft", key: "[" };
|
||||||
|
KEY_MAP["]"] = { code: "BracketRight", key: "]" };
|
||||||
|
KEY_MAP["."] = { code: "Period", key: "." };
|
||||||
|
KEY_MAP["`"] = { code: "Backquote", key: "`" };
|
||||||
|
KEY_MAP["/"] = { code: "Slash" };
|
||||||
|
KEY_MAP["?"] = { code: "Slash", key: "?" };
|
||||||
|
KEY_MAP["Space"] = { code: "Space", key: " " };
|
||||||
|
KEY_MAP["Escape"] = { code: "Escape" };
|
||||||
|
KEY_MAP["ArrowLeft"] = { code: "ArrowLeft" };
|
||||||
|
KEY_MAP["ArrowRight"] = { code: "ArrowRight" };
|
||||||
|
KEY_MAP["ArrowUp"] = { code: "ArrowUp" };
|
||||||
|
KEY_MAP["ArrowDown"] = { code: "ArrowDown" };
|
||||||
|
|
||||||
|
const CODE_TO_KEY: Record<string, string> = {};
|
||||||
|
for (const [humanKey, mapping] of Object.entries(KEY_MAP)) {
|
||||||
|
if (!CODE_TO_KEY[mapping.code]) {
|
||||||
|
CODE_TO_KEY[mapping.code] = humanKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseShortcutString(s: string): KeyCombo {
|
||||||
|
const parts = s.split("+");
|
||||||
|
if (parts.length === 0 || parts.some((p) => p === "")) {
|
||||||
|
throw new Error(`Invalid shortcut string: "${s}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const combo: KeyCombo = { code: "" };
|
||||||
|
let keyPart: string | null = null;
|
||||||
|
|
||||||
|
for (const part of parts) {
|
||||||
|
switch (part) {
|
||||||
|
case "Cmd":
|
||||||
|
combo.meta = true;
|
||||||
|
break;
|
||||||
|
case "Ctrl":
|
||||||
|
combo.ctrl = true;
|
||||||
|
break;
|
||||||
|
case "Alt":
|
||||||
|
combo.alt = true;
|
||||||
|
break;
|
||||||
|
case "Shift":
|
||||||
|
combo.shift = true;
|
||||||
|
break;
|
||||||
|
case "Mod":
|
||||||
|
combo.mod = true;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (keyPart !== null) {
|
||||||
|
throw new Error(`Invalid shortcut string: "${s}" - multiple key parts`);
|
||||||
|
}
|
||||||
|
keyPart = part;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyPart === null) {
|
||||||
|
throw new Error(`Invalid shortcut string: "${s}" - no key part`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapping = KEY_MAP[keyPart];
|
||||||
|
if (!mapping) {
|
||||||
|
throw new Error(`Unknown key: "${keyPart}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
combo.code = mapping.code;
|
||||||
|
if (mapping.key !== undefined) {
|
||||||
|
combo.key = mapping.key;
|
||||||
|
}
|
||||||
|
|
||||||
|
return combo;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function keyComboToString(combo: KeyCombo): string {
|
||||||
|
const parts: string[] = [];
|
||||||
|
|
||||||
|
if (combo.mod) {
|
||||||
|
parts.push("Mod");
|
||||||
|
}
|
||||||
|
if (combo.ctrl) parts.push("Ctrl");
|
||||||
|
if (combo.alt) parts.push("Alt");
|
||||||
|
if (combo.shift) parts.push("Shift");
|
||||||
|
if (combo.meta) parts.push("Cmd");
|
||||||
|
|
||||||
|
const humanKey = CODE_TO_KEY[combo.code];
|
||||||
|
if (humanKey) {
|
||||||
|
parts.push(humanKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.join("+");
|
||||||
|
}
|
||||||
|
|
||||||
|
const MODIFIER_CODES = new Set([
|
||||||
|
"MetaLeft",
|
||||||
|
"MetaRight",
|
||||||
|
"ControlLeft",
|
||||||
|
"ControlRight",
|
||||||
|
"AltLeft",
|
||||||
|
"AltRight",
|
||||||
|
"ShiftLeft",
|
||||||
|
"ShiftRight",
|
||||||
|
]);
|
||||||
|
|
||||||
|
export function comboStringToShortcutKeys(comboString: string): ShortcutKey[] {
|
||||||
|
const parts = comboString.split("+");
|
||||||
|
const keys: ShortcutKey[] = [];
|
||||||
|
for (const part of parts) {
|
||||||
|
switch (part) {
|
||||||
|
case "Cmd":
|
||||||
|
keys.push("mod");
|
||||||
|
break;
|
||||||
|
case "Ctrl":
|
||||||
|
keys.push("ctrl");
|
||||||
|
break;
|
||||||
|
case "Alt":
|
||||||
|
keys.push("alt");
|
||||||
|
break;
|
||||||
|
case "Shift":
|
||||||
|
keys.push("shift");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
keys.push(part.toUpperCase());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function keyboardEventToComboString(event: KeyboardEvent): string | null {
|
||||||
|
if (MODIFIER_CODES.has(event.code)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts: string[] = [];
|
||||||
|
|
||||||
|
if (event.ctrlKey) parts.push("Ctrl");
|
||||||
|
if (event.altKey) parts.push("Alt");
|
||||||
|
if (event.shiftKey) parts.push("Shift");
|
||||||
|
if (event.metaKey) parts.push("Cmd");
|
||||||
|
|
||||||
|
const humanKey = CODE_TO_KEY[event.code];
|
||||||
|
if (humanKey) {
|
||||||
|
parts.push(humanKey);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.join("+");
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,217 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { View, Text, Platform } from "react-native";
|
||||||
|
import { StyleSheet } from "react-native-unistyles";
|
||||||
|
import { settingsStyles } from "@/styles/settings";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Shortcut } from "@/components/ui/shortcut";
|
||||||
|
import { useKeyboardShortcutOverrides } from "@/hooks/use-keyboard-shortcut-overrides";
|
||||||
|
import {
|
||||||
|
buildKeyboardShortcutHelpSections,
|
||||||
|
getBindingIdForAction,
|
||||||
|
type KeyboardShortcutHelpRow,
|
||||||
|
} from "@/keyboard/keyboard-shortcuts";
|
||||||
|
import { comboStringToShortcutKeys, keyboardEventToComboString } from "@/keyboard/shortcut-string";
|
||||||
|
import { getShortcutOs } from "@/utils/shortcut-platform";
|
||||||
|
import { getIsDesktop } from "@/constants/layout";
|
||||||
|
|
||||||
|
function ShortcutRow({
|
||||||
|
row,
|
||||||
|
bindingId,
|
||||||
|
overrideCombo,
|
||||||
|
isCapturing,
|
||||||
|
onRebind,
|
||||||
|
onCancel,
|
||||||
|
onReset,
|
||||||
|
}: {
|
||||||
|
row: KeyboardShortcutHelpRow;
|
||||||
|
bindingId: string | null;
|
||||||
|
overrideCombo: string | undefined;
|
||||||
|
isCapturing: boolean;
|
||||||
|
onRebind: () => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
onReset: () => void;
|
||||||
|
}) {
|
||||||
|
const displayKeys = overrideCombo ? comboStringToShortcutKeys(overrideCombo) : row.keys;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[styles.row, isCapturing && styles.rowCapturing]}>
|
||||||
|
<Text style={styles.rowLabel}>{row.label}</Text>
|
||||||
|
<View style={styles.rowActions}>
|
||||||
|
{isCapturing ? (
|
||||||
|
<Text style={styles.capturingText}>Press shortcut...</Text>
|
||||||
|
) : (
|
||||||
|
<Shortcut keys={displayKeys} />
|
||||||
|
)}
|
||||||
|
{bindingId !== null && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onPress={isCapturing ? onCancel : onRebind}
|
||||||
|
>
|
||||||
|
{isCapturing ? "Cancel" : "Rebind"}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{overrideCombo !== undefined && !isCapturing && (
|
||||||
|
<Button variant="ghost" size="sm" onPress={onReset}>
|
||||||
|
<Text style={styles.resetText}>Reset</Text>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function KeyboardShortcutsSection() {
|
||||||
|
const [capturingBindingId, setCapturingBindingId] = useState<string | null>(null);
|
||||||
|
const { overrides, hasOverrides, setOverride, removeOverride, resetAll } =
|
||||||
|
useKeyboardShortcutOverrides();
|
||||||
|
|
||||||
|
const isMac = getShortcutOs() === "mac";
|
||||||
|
const isDesktop = getIsDesktop();
|
||||||
|
const sections = buildKeyboardShortcutHelpSections({ isMac, isDesktop });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (Platform.OS !== "web") return;
|
||||||
|
if (capturingBindingId === null) return;
|
||||||
|
|
||||||
|
const activeBindingId = capturingBindingId;
|
||||||
|
|
||||||
|
function handleKeyDown(event: KeyboardEvent) {
|
||||||
|
const key = event.key ?? "";
|
||||||
|
if (key === "Escape") {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
setCapturingBindingId(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === "Alt" || key === "Control" || key === "Meta" || key === "Shift") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const comboString = keyboardEventToComboString(event);
|
||||||
|
if (comboString === null) return;
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
void setOverride(activeBindingId, comboString);
|
||||||
|
setCapturingBindingId(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("keydown", handleKeyDown, true);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("keydown", handleKeyDown, true);
|
||||||
|
};
|
||||||
|
}, [capturingBindingId, setOverride]);
|
||||||
|
|
||||||
|
if (Platform.OS !== "web") {
|
||||||
|
return (
|
||||||
|
<View style={settingsStyles.section}>
|
||||||
|
<Text style={settingsStyles.sectionTitle}>Keyboard Shortcuts</Text>
|
||||||
|
<View style={[settingsStyles.card, styles.mobileCard]}>
|
||||||
|
<Text style={styles.mobileText}>
|
||||||
|
Keyboard shortcuts are only available on desktop.
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={settingsStyles.section}>
|
||||||
|
<View style={styles.sectionHeader}>
|
||||||
|
<Text style={settingsStyles.sectionTitle}>Keyboard Shortcuts</Text>
|
||||||
|
{hasOverrides && (
|
||||||
|
<Button variant="ghost" size="sm" onPress={() => void resetAll()}>
|
||||||
|
Reset all
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{sections.map(function (section) {
|
||||||
|
return (
|
||||||
|
<View key={section.id}>
|
||||||
|
<Text style={styles.subsectionTitle}>{section.title}</Text>
|
||||||
|
<View style={settingsStyles.card}>
|
||||||
|
{section.rows.map(function (row, index) {
|
||||||
|
const bindingId = getBindingIdForAction(row.id, { isMac, isDesktop });
|
||||||
|
const overrideCombo = bindingId ? overrides[bindingId] : undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View key={row.id}>
|
||||||
|
<ShortcutRow
|
||||||
|
row={row}
|
||||||
|
bindingId={bindingId}
|
||||||
|
overrideCombo={overrideCombo}
|
||||||
|
isCapturing={capturingBindingId === bindingId}
|
||||||
|
onRebind={() => setCapturingBindingId(bindingId)}
|
||||||
|
onCancel={() => setCapturingBindingId(null)}
|
||||||
|
onReset={() => {
|
||||||
|
if (bindingId) void removeOverride(bindingId);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{index < section.rows.length - 1 && <View style={styles.separator} />}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create((theme) => ({
|
||||||
|
sectionHeader: {
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
subsectionTitle: {
|
||||||
|
color: theme.colors.foregroundMuted,
|
||||||
|
fontSize: theme.fontSize.xs,
|
||||||
|
fontWeight: theme.fontWeight.normal,
|
||||||
|
marginBottom: theme.spacing[2],
|
||||||
|
marginTop: theme.spacing[4],
|
||||||
|
marginLeft: theme.spacing[1],
|
||||||
|
},
|
||||||
|
row: {
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
paddingHorizontal: theme.spacing[3],
|
||||||
|
paddingVertical: theme.spacing[3],
|
||||||
|
},
|
||||||
|
rowCapturing: {
|
||||||
|
backgroundColor: theme.colors.surface2,
|
||||||
|
},
|
||||||
|
rowLabel: {
|
||||||
|
fontSize: theme.fontSize.sm,
|
||||||
|
color: theme.colors.foreground,
|
||||||
|
flexShrink: 1,
|
||||||
|
},
|
||||||
|
rowActions: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: theme.spacing[2],
|
||||||
|
},
|
||||||
|
capturingText: {
|
||||||
|
fontSize: theme.fontSize.sm,
|
||||||
|
color: theme.colors.foregroundMuted,
|
||||||
|
},
|
||||||
|
resetText: {
|
||||||
|
color: theme.colors.foregroundMuted,
|
||||||
|
},
|
||||||
|
separator: {
|
||||||
|
height: 1,
|
||||||
|
backgroundColor: theme.colors.border,
|
||||||
|
},
|
||||||
|
mobileCard: {
|
||||||
|
padding: theme.spacing[4],
|
||||||
|
},
|
||||||
|
mobileText: {
|
||||||
|
fontSize: theme.fontSize.sm,
|
||||||
|
color: theme.colors.foregroundMuted,
|
||||||
|
},
|
||||||
|
}));
|
||||||
Reference in New Issue
Block a user