485 lines · 14.1 KB · TypeScript
RawDownload
import {  type KeybindingCommand,  type KeybindingShortcut,  type KeybindingWhenNode,  MODEL_PICKER_JUMP_KEYBINDING_COMMANDS,  type ResolvedKeybindingsConfig,  THREAD_JUMP_KEYBINDING_COMMANDS,  type ModelPickerJumpKeybindingCommand,  type ThreadJumpKeybindingCommand,} from "@t3tools/contracts";import { isMacPlatform } from "./lib/utils"; export interface ShortcutEventLike {  type?: string;  code?: string;  key: string;  metaKey: boolean;  ctrlKey: boolean;  shiftKey: boolean;  altKey: boolean;} export interface ShortcutModifierStateLike {  metaKey: boolean;  ctrlKey: boolean;  shiftKey: boolean;  altKey: boolean;} export interface ShortcutMatchContext {  terminalFocus: boolean;  terminalOpen: boolean;  previewFocus: boolean;  previewOpen: boolean;  [key: string]: boolean;} interface ShortcutMatchOptions {  platform?: string;  context?: Partial<ShortcutMatchContext>;} interface ResolvedShortcutLabelOptions extends ShortcutMatchOptions {  platform?: string;} const TERMINAL_WORD_BACKWARD = "\u001bb";const TERMINAL_WORD_FORWARD = "\u001bf";const TERMINAL_LINE_START = "\u0001";const TERMINAL_LINE_END = "\u0005";const TERMINAL_DELETE_TO_LINE_START = "\u0015";const EVENT_CODE_SHORTCUT_KEYS: Readonly<Record<string, string>> = {  Backquote: "`",  Backslash: "\\",  BracketLeft: "[",  BracketRight: "]",  Comma: ",",  Digit0: "0",  Digit1: "1",  Digit2: "2",  Digit3: "3",  Digit4: "4",  Digit5: "5",  Digit6: "6",  Digit7: "7",  Digit8: "8",  Digit9: "9",  Equal: "=",  Minus: "-",  Period: ".",  Quote: "'",  Semicolon: ";",  Slash: "/",}; function normalizeEventKey(key: string): string {  const normalized = key.toLowerCase();  if (normalized === "esc") return "escape";  return normalized;} export function shortcutKeyFromEvent(event: Pick<ShortcutEventLike, "key" | "code">): string {  const layoutKey = normalizeEventKey(event.key);  if (/^[a-z]$/.test(layoutKey)) return layoutKey;  const physicalKey = event.code ? EVENT_CODE_SHORTCUT_KEYS[event.code] : undefined;  return physicalKey ?? layoutKey;} function resolveEventKeys(event: ShortcutEventLike): Set<string> {  const layoutKey = normalizeEventKey(event.key);  const keys = new Set([layoutKey]);  // The physical-position fallback exists for layouts that type non-Latin  // letters (Cyrillic, Greek) and for Option-modified symbols on macOS.  // When the layout already produces a Latin letter, match on it alone;  // otherwise a remapped physical key triggers shortcuts for two different  // letters at once and shadows system shortcuts on non-QWERTY layouts.  const letterCode = event.code?.match(/^Key([A-Z])$/)?.[1];  if (letterCode && !/^[a-z]$/.test(layoutKey)) {    keys.add(letterCode.toLowerCase());  }  keys.add(shortcutKeyFromEvent(event));  return keys;} function matchesShortcutModifiers(  event: ShortcutModifierStateLike,  shortcut: KeybindingShortcut,  platform = navigator.platform,): boolean {  const useMetaForMod = isMacPlatform(platform);  const expectedMeta = shortcut.metaKey || (shortcut.modKey && useMetaForMod);  const expectedCtrl = shortcut.ctrlKey || (shortcut.modKey && !useMetaForMod);  return (    event.metaKey === expectedMeta &&    event.ctrlKey === expectedCtrl &&    event.shiftKey === shortcut.shiftKey &&    event.altKey === shortcut.altKey  );} function matchesShortcut(  event: ShortcutEventLike,  shortcut: KeybindingShortcut,  platform = navigator.platform,): boolean {  if (!matchesShortcutModifiers(event, shortcut, platform)) return false;  return resolveEventKeys(event).has(shortcut.key);} function resolvePlatform(options: ShortcutMatchOptions | undefined): string {  return options?.platform ?? navigator.platform;} function resolveContext(options: ShortcutMatchOptions | undefined): ShortcutMatchContext {  return {    terminalFocus: false,    terminalOpen: false,    previewFocus: false,    previewOpen: false,    ...options?.context,  };} function evaluateWhenNode(node: KeybindingWhenNode, context: ShortcutMatchContext): boolean {  switch (node.type) {    case "identifier":      if (node.name === "true") return true;      if (node.name === "false") return false;      return Boolean(context[node.name]);    case "not":      return !evaluateWhenNode(node.node, context);    case "and":      return evaluateWhenNode(node.left, context) && evaluateWhenNode(node.right, context);    case "or":      return evaluateWhenNode(node.left, context) || evaluateWhenNode(node.right, context);  }} function matchesWhenClause(  whenAst: KeybindingWhenNode | undefined,  context: ShortcutMatchContext,): boolean {  if (!whenAst) return true;  return evaluateWhenNode(whenAst, context);} export function shortcutConflictKey(  shortcut: KeybindingShortcut,  platform = navigator.platform,): string {  const useMetaForMod = isMacPlatform(platform);  const metaKey = shortcut.metaKey || (shortcut.modKey && useMetaForMod);  const ctrlKey = shortcut.ctrlKey || (shortcut.modKey && !useMetaForMod);  return [    shortcut.key,    metaKey ? "meta" : "",    ctrlKey ? "ctrl" : "",    shortcut.shiftKey ? "shift" : "",    shortcut.altKey ? "alt" : "",  ].join("|");} function findEffectiveShortcutForCommand(  keybindings: ResolvedKeybindingsConfig,  command: KeybindingCommand,  options?: ShortcutMatchOptions,): KeybindingShortcut | null {  const platform = resolvePlatform(options);  const context = resolveContext(options);  const claimedShortcuts = new Set<string>();   for (let index = keybindings.length - 1; index >= 0; index -= 1) {    const binding = keybindings[index];    if (!binding) continue;    if (!matchesWhenClause(binding.whenAst, context)) continue;     const conflictKey = shortcutConflictKey(binding.shortcut, platform);    if (claimedShortcuts.has(conflictKey)) {      continue;    }     claimedShortcuts.add(conflictKey);    if (binding.command === command) {      return binding.shortcut;    }  }   return null;} function matchesCommandShortcut(  event: ShortcutEventLike,  keybindings: ResolvedKeybindingsConfig,  command: KeybindingCommand,  options?: ShortcutMatchOptions,): boolean {  return resolveShortcutCommand(event, keybindings, options) === command;} export function resolveShortcutCommand(  event: ShortcutEventLike,  keybindings: ResolvedKeybindingsConfig,  options?: ShortcutMatchOptions,): KeybindingCommand | null {  const platform = resolvePlatform(options);  const context = resolveContext(options);   for (let index = keybindings.length - 1; index >= 0; index -= 1) {    const binding = keybindings[index];    if (!binding) continue;    if (!matchesWhenClause(binding.whenAst, context)) continue;    if (!matchesShortcut(event, binding.shortcut, platform)) continue;    return binding.command;  }  return null;} export function formatShortcutKeyLabel(key: string): string {  if (key === " ") return "Space";  if (key.length === 1) return key.toUpperCase();  if (key === "escape") return "Esc";  if (key === "arrowup") return "Up";  if (key === "arrowdown") return "Down";  if (key === "arrowleft") return "Left";  if (key === "arrowright") return "Right";  return key.slice(0, 1).toUpperCase() + key.slice(1);} export function formatShortcutLabel(  shortcut: KeybindingShortcut,  platform = navigator.platform,): string {  const keyLabel = formatShortcutKeyLabel(shortcut.key);  const useMetaForMod = isMacPlatform(platform);  const showMeta = shortcut.metaKey || (shortcut.modKey && useMetaForMod);  const showCtrl = shortcut.ctrlKey || (shortcut.modKey && !useMetaForMod);  const showAlt = shortcut.altKey;  const showShift = shortcut.shiftKey;   if (useMetaForMod) {    return `${showCtrl ? "\u2303" : ""}${showAlt ? "\u2325" : ""}${showShift ? "\u21e7" : ""}${showMeta ? "\u2318" : ""}${keyLabel}`;  }   const parts: string[] = [];  if (showCtrl) parts.push("Ctrl");  if (showAlt) parts.push("Alt");  if (showShift) parts.push("Shift");  if (showMeta) parts.push("Meta");  parts.push(keyLabel);  return parts.join("+");} export function shortcutLabelForCommand(  keybindings: ResolvedKeybindingsConfig,  command: KeybindingCommand | null,  options?: string | ResolvedShortcutLabelOptions,): string | null {  if (command === null) return null;  const resolvedOptions =    typeof options === "string"      ? ({ platform: options } satisfies ResolvedShortcutLabelOptions)      : options;  const platform = resolvePlatform(resolvedOptions);  const shortcut = findEffectiveShortcutForCommand(keybindings, command, resolvedOptions);  return shortcut ? formatShortcutLabel(shortcut, platform) : null;} export function threadJumpCommandForIndex(index: number): ThreadJumpKeybindingCommand | null {  return THREAD_JUMP_KEYBINDING_COMMANDS[index] ?? null;} export function threadJumpIndexFromCommand(command: string): number | null {  const index = THREAD_JUMP_KEYBINDING_COMMANDS.indexOf(command as ThreadJumpKeybindingCommand);  return index === -1 ? null : index;} export function threadTraversalDirectionFromCommand(  command: string | null,): "previous" | "next" | null {  if (command === "thread.previous") return "previous";  if (command === "thread.next") return "next";  return null;} export function shouldShowThreadJumpHintsForModifiers(  modifiers: ShortcutModifierStateLike,  keybindings: ResolvedKeybindingsConfig,  options?: ShortcutMatchOptions,): boolean {  // The embedded terminal owns keystrokes while it has focus: the Ghostty  // surface encodes the keydown and can write the pressed key into the shell  // before our window-level shortcut handling ever runs, regardless of any  // configured `when` clause on the jump command. Advertising jump hints  // here would promise a shortcut that instead types into the terminal, so  // never show them while the terminal is focused.  if (resolveContext(options).terminalFocus) {    return false;  }   const platform = resolvePlatform(options);   for (const command of THREAD_JUMP_KEYBINDING_COMMANDS) {    const shortcut = findEffectiveShortcutForCommand(keybindings, command, options);    if (!shortcut) continue;    if (matchesShortcutModifiers(modifiers, shortcut, platform)) {      return true;    }  }   return false;} export function modelPickerJumpCommandForIndex(  index: number,): ModelPickerJumpKeybindingCommand | null {  return MODEL_PICKER_JUMP_KEYBINDING_COMMANDS[index] ?? null;} export function modelPickerJumpIndexFromCommand(command: string): number | null {  const index = MODEL_PICKER_JUMP_KEYBINDING_COMMANDS.indexOf(    command as ModelPickerJumpKeybindingCommand,  );  return index === -1 ? null : index;} export function isTerminalToggleShortcut(  event: ShortcutEventLike,  keybindings: ResolvedKeybindingsConfig,  options?: ShortcutMatchOptions,): boolean {  return matchesCommandShortcut(event, keybindings, "terminal.toggle", options);} export function isTerminalSplitShortcut(  event: ShortcutEventLike,  keybindings: ResolvedKeybindingsConfig,  options?: ShortcutMatchOptions,): boolean {  return matchesCommandShortcut(event, keybindings, "terminal.split", options);} export function isTerminalSplitVerticalShortcut(  event: ShortcutEventLike,  keybindings: ResolvedKeybindingsConfig,  options?: ShortcutMatchOptions,): boolean {  return matchesCommandShortcut(event, keybindings, "terminal.splitVertical", options);} export function isTerminalNewShortcut(  event: ShortcutEventLike,  keybindings: ResolvedKeybindingsConfig,  options?: ShortcutMatchOptions,): boolean {  return matchesCommandShortcut(event, keybindings, "terminal.new", options);} export function isTerminalCloseShortcut(  event: ShortcutEventLike,  keybindings: ResolvedKeybindingsConfig,  options?: ShortcutMatchOptions,): boolean {  return matchesCommandShortcut(event, keybindings, "terminal.close", options);} export function isDiffToggleShortcut(  event: ShortcutEventLike,  keybindings: ResolvedKeybindingsConfig,  options?: ShortcutMatchOptions,): boolean {  return matchesCommandShortcut(event, keybindings, "diff.toggle", options);} export function isOpenFavoriteEditorShortcut(  event: ShortcutEventLike,  keybindings: ResolvedKeybindingsConfig,  options?: ShortcutMatchOptions,): boolean {  return matchesCommandShortcut(event, keybindings, "editor.openFavorite", options);} export function isTerminalClearShortcut(  event: ShortcutEventLike,  platform = navigator.platform,): boolean {  if (event.type !== undefined && event.type !== "keydown") {    return false;  }   const key = event.key.toLowerCase();   if (key === "l" && event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey) {    return true;  }   return (    isMacPlatform(platform) &&    key === "k" &&    event.metaKey &&    !event.ctrlKey &&    !event.altKey &&    !event.shiftKey  );} export function terminalDeleteShortcutData(  event: ShortcutEventLike,  platform = navigator.platform,): string | null {  if (event.type !== undefined && event.type !== "keydown") {    return null;  }   if (!isMacPlatform(platform)) {    return null;  }   const key = normalizeEventKey(event.key);  if (key !== "backspace") {    return null;  }   return event.metaKey && !event.ctrlKey && !event.altKey && !event.shiftKey    ? TERMINAL_DELETE_TO_LINE_START    : null;} export function terminalNavigationShortcutData(  event: ShortcutEventLike,  platform = navigator.platform,): string | null {  if (event.type !== undefined && event.type !== "keydown") {    return null;  }   if (event.shiftKey) return null;   const key = normalizeEventKey(event.key);  if (key !== "arrowleft" && key !== "arrowright") {    return null;  }   const moveWord = key === "arrowleft" ? TERMINAL_WORD_BACKWARD : TERMINAL_WORD_FORWARD;  const moveLine = key === "arrowleft" ? TERMINAL_LINE_START : TERMINAL_LINE_END;   if (isMacPlatform(platform)) {    if (event.altKey && !event.metaKey && !event.ctrlKey) {      return moveWord;    }    if (event.metaKey && !event.altKey && !event.ctrlKey) {      return moveLine;    }    return null;  }   if (event.ctrlKey && !event.metaKey && !event.altKey) {    return moveWord;  }   if (event.altKey && !event.metaKey && !event.ctrlKey) {    return moveWord;  }   return null;}