Files
PodTui/src/context/KeybindContext.tsx

130 lines
3.7 KiB
TypeScript

import { createMemo } from "solid-js"
import type { ParsedKey, Renderable } from "@opentui/core"
import { createStore } from "solid-js/store"
import { useKeyboard, useRenderer } from "@opentui/solid"
import { createSimpleContext } from "./helper"
import { Keybind, DEFAULT_KEYBINDS, type KeybindsConfig } from "../utils/keybind"
/**
* Keybind context provider for managing keyboard shortcuts.
*
* Features:
* - Leader key support (like vim's leader key)
* - Configurable keybindings
* - Key parsing and matching
* - Display-friendly key representations
*/
export const { use: useKeybind, provider: KeybindProvider } = createSimpleContext({
name: "Keybind",
init: (props: { keybinds?: Partial<KeybindsConfig> }) => {
// Merge default keybinds with custom keybinds
const customKeybinds = props.keybinds ?? {}
const mergedKeybinds = { ...DEFAULT_KEYBINDS, ...customKeybinds }
const keybinds = createMemo(() => {
const result: Record<string, Keybind.Info[]> = {}
for (const [key, value] of Object.entries(mergedKeybinds)) {
result[key] = Keybind.parse(value)
}
return result
})
const [store, setStore] = createStore({
leader: false,
})
const renderer = useRenderer()
let focus: Renderable | null = null
let timeout: NodeJS.Timeout | undefined
function leader(active: boolean) {
if (active) {
setStore("leader", true)
focus = renderer.currentFocusedRenderable
focus?.blur()
if (timeout) clearTimeout(timeout)
timeout = setTimeout(() => {
if (!store.leader) return
leader(false)
if (!focus || focus.isDestroyed) return
focus.focus()
}, 2000) // Leader key timeout
return
}
if (!active) {
if (focus && !renderer.currentFocusedRenderable) {
focus.focus()
}
setStore("leader", false)
}
}
// Handle leader key
useKeyboard(async (evt) => {
if (!store.leader && result.match("leader", evt)) {
leader(true)
return
}
if (store.leader && evt.name) {
setImmediate(() => {
if (focus && renderer.currentFocusedRenderable === focus) {
focus.focus()
}
leader(false)
})
}
})
const result = {
get all() {
return keybinds()
},
get leader() {
return store.leader
},
/**
* Parse a keyboard event into a Keybind.Info.
*/
parse(evt: ParsedKey): Keybind.Info {
// Handle special case for Ctrl+Underscore (represented as \x1F)
if (evt.name === "\x1F") {
return Keybind.fromParsedKey({ ...evt, name: "_", ctrl: true }, store.leader)
}
return Keybind.fromParsedKey(evt, store.leader)
},
/**
* Check if a keyboard event matches a registered keybind.
*/
match(key: keyof KeybindsConfig, evt: ParsedKey): boolean {
const keybind = keybinds()[key]
if (!keybind) return false
const parsed: Keybind.Info = result.parse(evt)
for (const kb of keybind) {
if (Keybind.match(kb, parsed)) {
return true
}
}
return false
},
/**
* Get a display string for a registered keybind.
*/
print(key: keyof KeybindsConfig): string {
const first = keybinds()[key]?.at(0)
if (!first) return ""
const display = Keybind.toString(first)
// Replace leader placeholder with actual leader key
const leaderKey = keybinds().leader?.[0]
if (leaderKey) {
return display.replace("<leader>", Keybind.toString(leaderKey))
}
return display
},
}
return result
},
})