import type { Key } from 'ink'; import type { AuditTuiAction, PaneId } from './audit-state'; import { PANE_BY_NUMBER } from './audit-state'; export interface KeymapResult { actions: AuditTuiAction[]; /** App-level signals the reducer can't express. */ signal?: 'quit' | 'help' | 'remediate' | 'reaudit' | 'copy' | 'toggle-theme'; } /** * Translate one keypress (input string + ink Key flags) into reducer * actions and/or app-level signals. Pure function — no side effects, * no React. * * Ink's `useInput(handler)` calls `handler(input, key)` for every * keypress; this function is what `handler` delegates to. */ export function handleKey(input: string, key: Key, focused: PaneId): KeymapResult { // Quit if (key.ctrl && input === 'c') return { actions: [], signal: 'quit' }; if (input === 'q' && !key.ctrl && !key.meta) return { actions: [], signal: 'quit' }; // Help overlay if (input === '?') return { actions: [{ type: 'toggle-help' }] }; // Pane jump (1-5) if (input === '1' || input === '2' || input === '3' || input === '4' || input === '5') { return { actions: [{ type: 'focus', pane: PANE_BY_NUMBER[input] }] }; } // Tab / Shift-Tab: cycle focus if (key.tab) { return { actions: [{ type: 'cycle', direction: key.shift ? -1 : 1 }] }; } // Movement: arrows + j/k if (key.upArrow || (input === 'k' && !key.ctrl)) { return { actions: [{ type: 'move', direction: -1 }] }; } if (key.downArrow || (input === 'j' && !key.ctrl)) { return { actions: [{ type: 'move', direction: 1 }] }; } // Enter: drill in if (key.return) { return { actions: [{ type: 'enter' }] }; } // Escape: step back; on top-of-chain panes, signal quit if (key.escape) { if (focused === 'summary' || focused === 'categories') { return { actions: [], signal: 'quit' }; } return { actions: [{ type: 'escape' }] }; } // Action hotkeys if (input === 'r' && !key.shift) return { actions: [], signal: 'remediate' }; if (input === 'R' || (input === 'r' && key.shift)) return { actions: [], signal: 'reaudit' }; // Yank — clipboard copy. Parent decides what to copy based on the // focused pane (detail content, command log, etc.). if (input === 'y' && !key.shift) return { actions: [], signal: 'copy' }; // Theme toggle. Routed as a signal so the parent can also flash a // confirmation in the keybar — the user otherwise has no visual cue // that anything happened (theme currently only drives modal colors). if (input === 't' && !key.shift) return { actions: [], signal: 'toggle-theme' }; return { actions: [] }; }