/** * Keyboard routing. * * The bindings are carried over verbatim from the Bubble Tea version — vim * motions alongside the arrows, shifted HJKL to resize a tile, `q` meaning * "quality" inside the editor but "quit" at the menu — so existing muscle * memory keeps working. `S` saves from anywhere, which is new. * * Matching is on `key.sequence` for printable keys because it preserves case * (`key.name` reports "l" for both `l` and `L`, which would collapse "move * right" into "make wider"), and on `key.name` for the named keys. */ import type { KeyEvent } from "@opentui/core" import type { Store } from "./state.ts" /** What the key handler needs from the UI: menu activation. */ export interface MenuActivator { activateMenu(idx: number): void } export function handleKey(store: Store, ui: MenuActivator, key: KeyEvent): void { // Ignore input while a discover or save is in flight, rather than queueing // keys that would act on state the bridge call is about to replace. if (store.busy) return const name = key.name const seq = key.sequence const back = name === "escape" || name === "backspace" switch (store.screen) { case "menu": { const n = store.listLength() if (name === "up" || seq === "k") store.moveCursor(-1, n) else if (name === "down" || seq === "j") store.moveCursor(1, n) else if (name === "return" || name === "space" || seq === "l") ui.activateMenu(store.cursor) else if (seq === "q") store.requestQuit() else if (seq === "S") void store.save() return } case "cameras": { const n = store.listLength() if (back || seq === "h") store.go("menu") else if (name === "up" || seq === "k") store.moveCursor(-1, n) else if (name === "down" || seq === "j") store.moveCursor(1, n) else if (seq === "d") store.toggleCameraDisabled() else if (seq === "x") store.deleteCamera() else if (seq === "S") void store.save() return } case "layouts": { const n = store.listLength() if (back || seq === "h") store.go("menu") else if (name === "up" || seq === "k") store.moveCursor(-1, n) else if (name === "down" || seq === "j") store.moveCursor(1, n) else if (name === "return" || seq === "l") store.openEditor(store.cursor) else if (seq === "S") void store.save() return } case "setActive": { const n = store.listLength() if (back || seq === "h") store.go("menu") else if (name === "up" || seq === "k") store.moveCursor(-1, n) else if (name === "down" || seq === "j") store.moveCursor(1, n) else if (name === "return" || seq === "l") store.setActiveLayout(store.cursor) return } case "cameraPicker": { const n = store.listLength() if (back || seq === "h") store.go("layoutEdit") else if (name === "up" || seq === "k") store.moveCursor(-1, n) else if (name === "down" || seq === "j") store.moveCursor(1, n) else if (name === "return" || seq === "l") store.choosePickerOption(store.cursor) return } case "layoutEdit": { // Returning to the layout list puts the cursor back on the layout that // was being edited, not at the top. if (back) store.go("layouts", store.editLayout) else if (name === "up" || seq === "k") store.moveCell(0, -1) else if (name === "down" || seq === "j") store.moveCell(0, 1) else if (name === "left" || seq === "h") store.moveCell(-1, 0) else if (name === "right" || seq === "l") store.moveCell(1, 0) else if (name === "return" || name === "space" || seq === "a") store.go("cameraPicker") else if (seq === "c") store.clearAt() else if (seq === "q") store.cycleQuality() // Shifted motions resize the tile; unshifted move the cursor. else if (seq === "L") store.resizeTile(1, 0) else if (seq === "H") store.resizeTile(-1, 0) else if (seq === "J") store.resizeTile(0, 1) else if (seq === "K") store.resizeTile(0, -1) else if (seq === "]") store.resizeGrid(1, 0) else if (seq === "[") store.resizeGrid(-1, 0) else if (seq === "}") store.resizeGrid(0, 1) else if (seq === "{") store.resizeGrid(0, -1) else if (seq === "S") void store.save() return } } }