keys

Key chords. Bindings do not fire while a palette is open.

interface KeysApi {
    // Bind a chord. Throws KEY_CHORD_TAKEN if the terminal or another
    // extension owns it.
    register(chord: string, handler: () => void | Promise<void>): Disposer

    // Deliver a keypress as though the user had typed it.
    // Throws KEY_CHORD_UNBOUND if nothing answers to it.
    send(chord: string): void
}

Binding

keys.register("ctrl+o", () => commands.run("session open"))

keys.register("ctrl+shift+r", async () => {
    await agents.reboot()
    tui.info("rebooted")
})

Chords are written the way they read — "a", "f5", "ctrl+o", "shift+tab", "ctrl+shift+p" — and matched case-insensitively.

Nothing waits on a handler; it cannot block the keyboard. A rejection is caught and reported as ctrl+o failed on the cwd row rather than taking the process down.

Reserved chords

ctrl+c   ctrl+d   escape   enter   tab
up   down   left   right   backspace   delete
:  ~  /  ^  *  %  >  "  #  ?
keys.register("ctrl+c", run)   // KEY_CHORD_TAKEN

Refused at registration, not at press time: user bindings dispatch from a wildcard handler that runs before exact-key ones, so a config that bound ctrl+c would beat the exit ladder. By then it is too late to defer.

Sending

keys.send("ctrl+o")   // a chord you bound
keys.send("&")        // a mode key your palette claimed
keys.send(":")        // a built-in mode key

The escape hatch that keeps the surface honest — anything the API forgot to expose is still reachable, so a missing verb is an inconvenience rather than a wall.

Guarding a mode key

keys.register("ctrl+b", async () => {
    if (palette.isOpen) return
    await palette.open("branches")
})

Observing instead of claiming

tui.hook("key:pressed", ({ key, mode }) => {
    if (mode === "normal" && key === "ctrl+g") openThing()
})

Fires only for keys nothing handled, and cannot consume them. key round-trips — keys.register(key, fn) accepts exactly what you receive.