import { Key, matchesKey } from "@mariozechner/pi-tui"; export type ListIntent = | { type: "cancel" } | { type: "searchStart" } | { type: "searchCancel" } | { type: "searchApply" } | { type: "searchBackspace" } | { type: "searchAppend"; value: string } | { type: "moveSelection"; delta: number } | { type: "work" } | { type: "edit" } | { type: "toggleStatus" } | { type: "setPriority"; priority: string } | { type: "scrollDescription"; delta: number } | { type: "toggleType" } | { type: "create" } | { type: "insert" } | { type: "delegate" }; export interface ListControllerState { searching: boolean; filtered: boolean; allowSearch: boolean; allowPriority: boolean; closeKey: string; priorities: string[]; priorityHotkeys?: Record; } type ShortcutContext = "default" | "search"; interface ShortcutDefinition { context: ShortcutContext; help?: string | ((state: ListControllerState) => string); showInHelp?: (state: ListControllerState) => boolean; match: (data: string, state: ListControllerState) => boolean; intent: (data: string, state: ListControllerState) => ListIntent; } function parsePriorityKey( data: string, priorities: string[], priorityHotkeys?: Record ): string | null { if (data.length !== 1) return null; const hotkeyPriority = priorityHotkeys?.[data]; if (hotkeyPriority && priorities.includes(hotkeyPriority)) return hotkeyPriority; const rank = parseInt(data, 10); if (isNaN(rank) || rank < 1 || rank > priorities.length) return null; return priorities[rank - 1] ?? null; } function buildPriorityHelpText( priorities: string[], priorityHotkeys?: Record ): string { const hotkeyKeys = priorityHotkeys ? Object.keys(priorityHotkeys).sort((a, b) => a.localeCompare(b)) : []; if (hotkeyKeys.length > 0) { return `${hotkeyKeys.join("/")} priority`; } if (priorities.length === 0) return "priority"; if (priorities.length === 1) return "1 priority"; return `1-${priorities.length} priority`; } function isPrintable(data: string): boolean { return data.length === 1 && data.charCodeAt(0) >= 32 && data.charCodeAt(0) < 127; } const MOVE_KEYS: Record = { w: -1, W: -1, s: 1, S: 1, }; const SCROLL_KEYS: Record = { j: 1, k: -1, }; const SHORTCUT_DEFINITIONS: ShortcutDefinition[] = [ { context: "search", match: (data, state) => data === state.closeKey, intent: () => ({ type: "cancel" }), }, { context: "search", help: "esc cancel", match: (data) => matchesKey(data, Key.escape), intent: () => ({ type: "searchCancel" }), }, { context: "search", help: "enter apply", match: (data) => matchesKey(data, Key.enter), intent: () => ({ type: "searchApply" }), }, { context: "search", match: (data) => matchesKey(data, Key.backspace), intent: () => ({ type: "searchBackspace" }), }, { context: "search", help: "type to search", match: (data) => isPrintable(data), intent: (data) => ({ type: "searchAppend", value: data }), }, { context: "default", help: "w/s navigate", match: (data) => data in MOVE_KEYS, intent: (data) => ({ type: "moveSelection", delta: MOVE_KEYS[data] ?? 1 }), }, { context: "default", help: "enter work", match: (data) => matchesKey(data, Key.enter), intent: () => ({ type: "work" }), }, { context: "default", help: "e edit", match: (data) => data === "e" || data === "E" || matchesKey(data, Key.right), intent: () => ({ type: "edit" }), }, { context: "default", help: (state) => buildPriorityHelpText(state.priorities, state.priorityHotkeys), showInHelp: (state) => state.allowPriority, match: (data, state) => state.allowPriority && parsePriorityKey(data, state.priorities, state.priorityHotkeys) !== null, intent: (data, state) => ({ type: "setPriority", priority: parsePriorityKey(data, state.priorities, state.priorityHotkeys) ?? state.priorities[0] ?? "", }), }, { context: "default", help: "f find", showInHelp: (state) => state.allowSearch, match: (data, state) => state.allowSearch && (data === "f" || data === "F"), intent: () => ({ type: "searchStart" }), }, { context: "default", match: (data) => data === " ", intent: () => ({ type: "toggleStatus" }), }, { context: "default", match: (data) => data in SCROLL_KEYS, intent: (data) => ({ type: "scrollDescription", delta: SCROLL_KEYS[data] ?? 1 }), }, { context: "default", help: "t type", match: (data) => data === "t" || data === "T", intent: () => ({ type: "toggleType" }), }, { context: "default", help: "c create", match: (data) => data === "c" || data === "C", intent: () => ({ type: "create" }), }, { context: "default", help: "tab insert", match: (data) => matchesKey(data, Key.tab), intent: () => ({ type: "insert" }), }, { context: "default", match: (data, state) => data === state.closeKey, intent: () => ({ type: "cancel" }), }, ]; export function resolveListIntent(data: string, state: ListControllerState): ListIntent { const context: ShortcutContext = state.searching ? "search" : "default"; for (const shortcut of SHORTCUT_DEFINITIONS) { if (shortcut.context !== context) continue; if (shortcut.match(data, state)) return shortcut.intent(data, state); } return { type: "delegate" }; } export function buildListPrimaryHelpText(state: ListControllerState): string { const context: ShortcutContext = state.searching ? "search" : "default"; const parts = SHORTCUT_DEFINITIONS.filter((s) => s.context === context) .filter((s) => !!s.help) .filter((s) => (s.showInHelp ? s.showInHelp(state) : true)) .map((s) => (typeof s.help === "function" ? s.help(state) : (s.help as string))); if (context === "default") { parts.push(state.filtered ? "esc clear filter" : "esc cancel"); } return parts.join(" • "); } export function buildListSecondaryHelpText(): string { return "space status • j/k scroll"; }