Julius Marmingedoc90750868 months agoHistory
355 lines · 9.2 KB · TypeScript
RawDownload
import fs from "node:fs";import os from "node:os";import path from "node:path"; import type {  KeybindingRule,  KeybindingsConfig,  KeybindingShortcut,  KeybindingWhenNode,  ResolvedKeybindingRule,  ResolvedKeybindingsConfig,} from "@t3tools/contracts";import { keybindingRuleSchema } from "@t3tools/contracts"; interface KeybindingsLogger {  warn(message: string, context?: Record<string, unknown>): void;} type WhenToken =  | { type: "identifier"; value: string }  | { type: "not" }  | { type: "and" }  | { type: "or" }  | { type: "lparen" }  | { type: "rparen" }; export const DEFAULT_KEYBINDINGS: ReadonlyArray<KeybindingRule> = [  { key: "mod+j", command: "terminal.toggle" },  { key: "mod+d", command: "terminal.split", when: "terminalFocus" },  { key: "mod+n", command: "terminal.new", when: "terminalFocus" },  { key: "mod+w", command: "terminal.close", when: "terminalFocus" },  { key: "mod+n", command: "chat.new", when: "!terminalFocus" },  { key: "mod+shift+o", command: "chat.new", when: "!terminalFocus" },  { key: "mod+shift+n", command: "chat.newLocal", when: "!terminalFocus" },  { key: "mod+o", command: "editor.openFavorite" },]; function normalizeKeyToken(token: string): string {  if (token === "space") return " ";  if (token === "esc") return "escape";  return token;} export function parseKeybindingShortcut(value: string): KeybindingShortcut | null {  const rawTokens = value    .toLowerCase()    .split("+")    .map((token) => token.trim());  const tokens = [...rawTokens];  let trailingEmptyCount = 0;  while (tokens[tokens.length - 1] === "") {    trailingEmptyCount += 1;    tokens.pop();  }  if (trailingEmptyCount > 0) {    tokens.push("+");  }  if (tokens.some((token) => token.length === 0)) {    return null;  }  if (tokens.length === 0) return null;   let key: string | null = null;  let metaKey = false;  let ctrlKey = false;  let shiftKey = false;  let altKey = false;  let modKey = false;   for (const token of tokens) {    switch (token) {      case "cmd":      case "meta":        metaKey = true;        break;      case "ctrl":      case "control":        ctrlKey = true;        break;      case "shift":        shiftKey = true;        break;      case "alt":      case "option":        altKey = true;        break;      case "mod":        modKey = true;        break;      default: {        if (key !== null) return null;        key = normalizeKeyToken(token);      }    }  }   if (key === null) return null;  return {    key,    metaKey,    ctrlKey,    shiftKey,    altKey,    modKey,  };} function tokenizeWhenExpression(expression: string): WhenToken[] | null {  const tokens: WhenToken[] = [];  let index = 0;   while (index < expression.length) {    const current = expression[index];    if (!current) break;     if (/\s/.test(current)) {      index += 1;      continue;    }    if (expression.startsWith("&&", index)) {      tokens.push({ type: "and" });      index += 2;      continue;    }    if (expression.startsWith("||", index)) {      tokens.push({ type: "or" });      index += 2;      continue;    }    if (current === "!") {      tokens.push({ type: "not" });      index += 1;      continue;    }    if (current === "(") {      tokens.push({ type: "lparen" });      index += 1;      continue;    }    if (current === ")") {      tokens.push({ type: "rparen" });      index += 1;      continue;    }     const identifier = /^[A-Za-z_][A-Za-z0-9_.-]*/.exec(expression.slice(index));    if (!identifier) {      return null;    }    tokens.push({ type: "identifier", value: identifier[0] });    index += identifier[0].length;  }   return tokens;} export function parseKeybindingWhenExpression(expression: string): KeybindingWhenNode | null {  const tokens = tokenizeWhenExpression(expression);  if (!tokens || tokens.length === 0) return null;  let index = 0;   const parsePrimary = (): KeybindingWhenNode | null => {    const token = tokens[index];    if (!token) return null;     if (token.type === "identifier") {      index += 1;      return { type: "identifier", name: token.value };    }     if (token.type === "lparen") {      index += 1;      const expressionNode = parseOr();      const closeToken = tokens[index];      if (!expressionNode || !closeToken || closeToken.type !== "rparen") {        return null;      }      index += 1;      return expressionNode;    }     return null;  };   const parseUnary = (): KeybindingWhenNode | null => {    const token = tokens[index];    if (token?.type === "not") {      index += 1;      const node = parseUnary();      if (!node) return null;      return { type: "not", node };    }    return parsePrimary();  };   const parseAnd = (): KeybindingWhenNode | null => {    let left = parseUnary();    if (!left) return null;     while (tokens[index]?.type === "and") {      index += 1;      const right = parseUnary();      if (!right) return null;      left = { type: "and", left, right };    }     return left;  };   const parseOr = (): KeybindingWhenNode | null => {    let left = parseAnd();    if (!left) return null;     while (tokens[index]?.type === "or") {      index += 1;      const right = parseAnd();      if (!right) return null;      left = { type: "or", left, right };    }     return left;  };   const ast = parseOr();  if (!ast || index !== tokens.length) return null;  return ast;} export function compileResolvedKeybindingRule(rule: KeybindingRule): ResolvedKeybindingRule | null {  const key = rule.key.trim();  if (key.length === 0) return null;  const shortcut = parseKeybindingShortcut(key);  if (!shortcut) return null;   const when = rule.when?.trim();  if (when && when.length > 0) {    const whenAst = parseKeybindingWhenExpression(when);    if (!whenAst) return null;    return {      command: rule.command,      shortcut,      whenAst,    };  }   return {    command: rule.command,    shortcut,  };} export function compileResolvedKeybindingsConfig(  config: KeybindingsConfig,): ResolvedKeybindingsConfig {  const compiled: ResolvedKeybindingsConfig = [];  for (const rule of config) {    const resolved = compileResolvedKeybindingRule(rule);    if (!resolved) continue;    compiled.push(resolved);  }  return compiled;} function compileDefaultKeybindings(): ResolvedKeybindingsConfig {  const resolved: ResolvedKeybindingsConfig = [];  for (const rule of DEFAULT_KEYBINDINGS) {    const compiled = compileResolvedKeybindingRule(rule);    if (!compiled) {      throw new Error(`Invalid default keybinding: ${rule.command} (${rule.key})`);    }    resolved.push(compiled);  }  return resolved;} const DEFAULT_RESOLVED_KEYBINDINGS = compileDefaultKeybindings(); function mergeWithDefaultKeybindings(custom: ResolvedKeybindingsConfig): ResolvedKeybindingsConfig {  if (custom.length === 0) {    return [...DEFAULT_RESOLVED_KEYBINDINGS];  }   const overriddenCommands = new Set(custom.map((binding) => binding.command));  const retainedDefaults = DEFAULT_RESOLVED_KEYBINDINGS.filter(    (binding) => !overriddenCommands.has(binding.command),  );  const merged = [...retainedDefaults, ...custom];   if (merged.length <= 256) {    return merged;  }   // Keep the latest rules when the config exceeds max size; later rules have higher precedence.  return merged.slice(-256);} export function loadResolvedKeybindingsConfig(  logger: KeybindingsLogger,): ResolvedKeybindingsConfig {  const configPath = path.join(os.homedir(), ".t3", "keybindings.json");  try {    const raw = fs.readFileSync(configPath, "utf8");    const parsed = JSON.parse(raw) as unknown;    if (!Array.isArray(parsed)) {      logger.warn("ignoring keybindings config with unsupported format; expected array", {        path: configPath,      });      return [...DEFAULT_RESOLVED_KEYBINDINGS];    }     const sanitized: ResolvedKeybindingsConfig = [];    let invalidEntries = 0;    for (const entry of parsed) {      const result = keybindingRuleSchema.safeParse(entry);      if (result.success) {        const compiled = compileResolvedKeybindingRule(result.data);        if (!compiled) {          invalidEntries += 1;          continue;        }        sanitized.push(compiled);        continue;      }      invalidEntries += 1;    }    if (invalidEntries > 0) {      logger.warn("ignoring invalid keybinding entries", {        path: configPath,        invalidEntries,        totalEntries: parsed.length,      });    }    const overriddenCommands = new Set(sanitized.map((entry) => entry.command));    const mergedBeforeCapLength =      DEFAULT_RESOLVED_KEYBINDINGS.filter((binding) => !overriddenCommands.has(binding.command))        .length + sanitized.length;    const merged = mergeWithDefaultKeybindings(sanitized);    if (mergedBeforeCapLength > 256) {      logger.warn("truncating merged keybindings config to max entries", {        path: configPath,        maxEntries: 256,      });    }    return merged;  } catch (error) {    if ((error as NodeJS.ErrnoException)?.code === "ENOENT") {      return [...DEFAULT_RESOLVED_KEYBINDINGS];    }    logger.warn("ignoring malformed keybindings config", {      path: configPath,      error: error instanceof Error ? error.message : String(error),    });  }  return [...DEFAULT_RESOLVED_KEYBINDINGS];}