keybind.ts 2.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103
  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 | undefined, b: Info): boolean {
  12. if (!a) return false
  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 | undefined): string {
  32. if (!info) return ""
  33. const parts: string[] = []
  34. if (info.ctrl) parts.push("ctrl")
  35. if (info.meta) parts.push("alt")
  36. if (info.super) parts.push("super")
  37. if (info.shift) parts.push("shift")
  38. if (info.name) {
  39. if (info.name === "delete") parts.push("del")
  40. else parts.push(info.name)
  41. }
  42. let result = parts.join("+")
  43. if (info.leader) {
  44. result = result ? `<leader> ${result}` : `<leader>`
  45. }
  46. return result
  47. }
  48. export function parse(key: string): Info[] {
  49. if (key === "none") return []
  50. return key.split(",").map((combo) => {
  51. // Handle <leader> syntax by replacing with leader+
  52. const normalized = combo.replace(/<leader>/g, "leader+")
  53. const parts = normalized.toLowerCase().split("+")
  54. const info: Info = {
  55. ctrl: false,
  56. meta: false,
  57. shift: false,
  58. leader: false,
  59. name: "",
  60. }
  61. for (const part of parts) {
  62. switch (part) {
  63. case "ctrl":
  64. info.ctrl = true
  65. break
  66. case "alt":
  67. case "meta":
  68. case "option":
  69. info.meta = true
  70. break
  71. case "super":
  72. info.super = true
  73. break
  74. case "shift":
  75. info.shift = true
  76. break
  77. case "leader":
  78. info.leader = true
  79. break
  80. case "esc":
  81. info.name = "escape"
  82. break
  83. default:
  84. info.name = part
  85. break
  86. }
  87. }
  88. return info
  89. })
  90. }
  91. }