keybind.ts 2.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. import { isDeepEqual } from "remeda"
  2. import type { ParsedKey } from "@opentui/core"
  3. export namespace Keybind {
  4. /**
  5. * Keybind info derived from OpenTUI's ParsedKey with our custom `leader` field.
  6. * This ensures type compatibility and catches missing fields at compile time.
  7. */
  8. export type Info = Pick<ParsedKey, "name" | "ctrl" | "meta" | "shift" | "super"> & {
  9. leader: boolean // our custom field
  10. }
  11. export function match(a: Info, b: Info): boolean {
  12. // Normalize super field (undefined and false are equivalent)
  13. const normalizedA = { ...a, super: a.super ?? false }
  14. const normalizedB = { ...b, super: b.super ?? false }
  15. return isDeepEqual(normalizedA, normalizedB)
  16. }
  17. /**
  18. * Convert OpenTUI's ParsedKey to our Keybind.Info format.
  19. * This helper ensures all required fields are present and avoids manual object creation.
  20. */
  21. export function fromParsedKey(key: ParsedKey, leader = false): Info {
  22. return {
  23. name: key.name,
  24. ctrl: key.ctrl,
  25. meta: key.meta,
  26. shift: key.shift,
  27. super: key.super ?? false,
  28. leader,
  29. }
  30. }
  31. export function toString(info: Info): string {
  32. const parts: string[] = []
  33. if (info.ctrl) parts.push("ctrl")
  34. if (info.meta) parts.push("alt")
  35. if (info.super) parts.push("super")
  36. if (info.shift) parts.push("shift")
  37. if (info.name) {
  38. if (info.name === "delete") parts.push("del")
  39. else parts.push(info.name)
  40. }
  41. let result = parts.join("+")
  42. if (info.leader) {
  43. result = result ? `<leader> ${result}` : `<leader>`
  44. }
  45. return result
  46. }
  47. export function parse(key: string): Info[] {
  48. if (key === "none") return []
  49. return key.split(",").map((combo) => {
  50. // Handle <leader> syntax by replacing with leader+
  51. const normalized = combo.replace(/<leader>/g, "leader+")
  52. const parts = normalized.toLowerCase().split("+")
  53. const info: Info = {
  54. ctrl: false,
  55. meta: false,
  56. shift: false,
  57. leader: false,
  58. name: "",
  59. }
  60. for (const part of parts) {
  61. switch (part) {
  62. case "ctrl":
  63. info.ctrl = true
  64. break
  65. case "alt":
  66. case "meta":
  67. case "option":
  68. info.meta = true
  69. break
  70. case "super":
  71. info.super = true
  72. break
  73. case "shift":
  74. info.shift = true
  75. break
  76. case "leader":
  77. info.leader = true
  78. break
  79. case "esc":
  80. info.name = "escape"
  81. break
  82. default:
  83. info.name = part
  84. break
  85. }
  86. }
  87. return info
  88. })
  89. }
  90. }