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(""); }