import { createContext, use, useState, type ReactNode } from "react"; import { View, Text } from "react-native"; import { useKeyboard } from "../src/useKeyboard"; import type { KeyEvent } from "@opentui/core"; const HEADER_COLOR = "#7158e2"; const TABLE_COLORS = ["#ddd", "#eee"]; const SELECTED_COLOR = "#f7b730"; const EXTRA = 5; export type ValidRecord = Record; interface TableState { data: unknown[]; columns: Column[]; columnMap: Map; idx: number; selectedFrom: number | undefined; } const INITAL_STATE = { data: [], columns: [], columnMap: new Map(), idx: 0, selectedFrom: undefined, } satisfies TableState; export const Context = createContext(INITAL_STATE); export type Column = { name: string; label: string; render?: (i: number | string) => string; }; function renderCell(row: ValidRecord, column: Column): string { const cell = row[column.name]; if (cell == undefined) return "n/a"; if (cell == null) return "null"; if (column.render) return column.render(cell); return cell.toString(); } export interface ProviderProps { data: T[]; columns: Column[]; children: ReactNode; onKey?: (event: KeyEvent, selected: T[]) => void; } export function Provider({ data, columns, children, onKey, }: ProviderProps) { const [idx, setIdx] = useState(0); const [selectedFrom, setSelectedFrom] = useState(); useKeyboard( (key) => { if (key.name == "j" || key.name == "down") { if (key.shift && selectedFrom == undefined) { setSelectedFrom(idx); } setIdx((prev) => Math.min(prev + 1, data.length - 1)); } else if (key.name == "k" || key.name == "up") { if (key.shift && selectedFrom == undefined) { setSelectedFrom(idx); } setIdx((prev) => Math.max(prev - 1, 0)); } else if (key.name == "g" && key.shift) { setIdx(data.length - 1); } else if (key.name == "v") { setSelectedFrom(idx); } else if (key.name == "escape") { setSelectedFrom(undefined); } else { const from = selectedFrom ? Math.min(idx, selectedFrom) : idx; const to = selectedFrom ? Math.max(idx, selectedFrom) : idx; const selected = data.slice(from, to + 1); if (onKey) onKey(key, selected); } }, [data, idx, selectedFrom], ); const columnMap = new Map( columns.map((col) => { return [ col.name, Math.max( col.label.length, ...data.map((row) => renderCell(row, col).length), ), ]; }), ); return ( {children} ); } export function Body() { const { columns, data, columnMap, idx, selectedFrom } = use(Context); return ( {columns.map((column) => ( {rpad( column.label, columnMap.get(column.name)! - column.label.length + EXTRA, )} ))} {data.map((row, index) => { const isSelected = index == idx || (selectedFrom != undefined && ((selectedFrom <= index && index <= idx) || (idx <= index && index <= selectedFrom))); return ( ); })} ); } interface RowProps { row: T; index: number; isSelected: boolean; } function TableRow({ row, isSelected }: RowProps) { const { columns, columnMap } = use(Context); return ( {columns.map((column) => { const rendered = renderCell(row, column); return ( {rpad( rendered, columnMap.get(column.name)! - rendered.length + EXTRA, )} ); })} ); } function rpad(input: string, length: number): string { return ( input + Array.from({ length }) .map((_) => " ") .join("") ); }