import { Box, render, Text } from "ink"; import type { ReactNode } from "react"; import type { CliError, CliResult } from "../context.ts"; import { dueHex, dueStatus, formatDue, labelHex, listAccentHex } from "./palette.ts"; import { isAction, isBoard, isCard, isChecklist, isLabel, isList, isMember, isMessagePayload, isProfilesPayload, isRecord, isSearchResult, isWebhook, type Rec, } from "./shapes.ts"; const TRELLO_BLUE = "#0079bf"; const GREEN = "#61bd4f"; const RED = "#eb5a46"; const YELLOW = "#f2d600"; function clip(text: string, max: number): string { return text.length <= max ? text : `${text.slice(0, Math.max(0, max - 1))}…`; } /** Render a result as a one-shot ink frame (colors auto-disable when piped). */ export async function renderResult(result: CliResult | CliError): Promise { const instance = render(, { exitOnCtrlC: false, patchConsole: false, }); instance.unmount(); await instance.waitUntilExit(); } function ResultView({ result }: { result: CliResult | CliError }) { if (!result.ok) { return ( ✗ {result.error} {result.status ? (HTTP {result.status}) : null} {result.details && isRecord(result.details) ? ( {clip(JSON.stringify(result.details), 120)} ) : null} ); } return ( {result.profile !== "default" ? ( profile: {result.profile} ) : null} ); } function ResultData({ data }: { data: unknown }) { if (Array.isArray(data)) return ; if (isSearchResult(data)) return ; if (isProfilesPayload(data)) return ; if (isMessagePayload(data)) return ; if (isCard(data)) return ; if (isChecklist(data)) return ; if (isMember(data)) return ; if (isBoard(data)) return ; if (isRecord(data)) return ; if (data === null || data === undefined) return (no data); return {String(data)}; } function ArrayView({ items }: { items: unknown[] }) { if (items.length === 0) return (none); const first = items[0]; let rows: ReactNode; if (isCard(first)) { rows = items.filter(isCard).map((c) => ); } else if (isList(first)) { rows = items.filter(isList).map((l) => ); } else if (isLabel(first)) { rows = items.filter(isLabel).map((l) => ); } else if (isBoard(first)) { rows = items.filter(isBoard).map((b) => ); } else if (isWebhook(first)) { rows = items .filter(isWebhook) .map((w) => ); } else if (isAction(first)) { rows = items .filter(isAction) .map((a) => ); } else { rows = items.map((item, i) => ( )); } return ( {rows} {items.length} total ); } function LabelDots({ labels }: { labels: unknown }) { if (!Array.isArray(labels) || labels.length === 0) return null; return ( <> {labels.filter(isRecord).map((label) => ( ●{" "} ))} ); } function DueBadge({ card }: { card: Rec }) { const due = card.due as string | null | undefined; if (!due) return null; const status = dueStatus(due, card.dueComplete as boolean | undefined); return ( {formatDue(due)} {status === "complete" ? " ✓" : ""}{" "} ); } function CardRow({ card }: { card: Rec }) { const badges = isRecord(card.badges) ? card.badges : undefined; const checkItems = (badges?.checkItems as number | undefined) ?? 0; const checked = (badges?.checkItemsChecked as number | undefined) ?? 0; const comments = (badges?.comments as number | undefined) ?? 0; const attachments = (badges?.attachments as number | undefined) ?? 0; return ( {card.dueComplete === true ? : null} {String(card.name)} {checkItems > 0 ? ( ✓{checked}/{checkItems}{" "} ) : null} {card.desc ? : null} {comments > 0 ? 💬{comments} : null} {attachments > 0 ? 📎{attachments} : null} {String(card.shortLink ?? card.id ?? "")} ); } function CardDetail({ card }: { card: Rec }) { const labels = Array.isArray(card.labels) ? card.labels.filter(isRecord) : []; const desc = typeof card.desc === "string" ? card.desc : ""; return ( {String(card.name)} {labels.length > 0 ? ( {labels.map((label) => ( {` ${String(label.name || label.color || "label")} `} ))} ) : null} {card.due ? ( Due: ) : null} {desc ? {clip(desc, 600)} : null} {String(card.shortUrl ?? card.url ?? card.id ?? "")} ); } function BoardRow({ board }: { board: Rec }) { return ( {String(board.name)} {board.closed === true ? (closed) : null}{" "} {String(board.shortUrl ?? board.id ?? "")} ); } function ListRow({ list }: { list: Rec }) { return ( {" "} {String(list.name)} {list.closed === true ? (archived) : null}{" "} {String(list.id ?? "")} ); } function LabelRow({ label }: { label: Rec }) { return ( {` ${String(label.name || label.color || "label")} `} {" "} {String(label.id ?? "")} ); } function WebhookRow({ webhook }: { webhook: Rec }) { return ( {String(webhook.description || webhook.callbackURL)}{" "} model {String(webhook.idModel ?? "")} · {String(webhook.id ?? "")} ); } function ActionRow({ action }: { action: Rec }) { const date = String(action.date ?? "") .slice(0, 16) .replace("T", " "); return ( {date} {String(action.type)} ); } function ChecklistView({ checklist }: { checklist: Rec }) { const items = (checklist.checkItems as unknown[]).filter(isRecord); return ( {String(checklist.name)} {items.map((item) => { const done = item.state === "complete"; return ( {done ? : "☐ "} {String(item.name)} ); })} ); } function MemberRow({ member }: { member: Rec }) { return ( @{String(member.username)} {member.fullName ? {String(member.fullName)} : null} {member.url ? {String(member.url)} : null} ); } function ProfilesView({ data }: { data: Rec }) { const profiles = (data.profiles as unknown[]).filter(isRecord); return ( {profiles.length === 0 ? ( No profiles. Run: trelly auth login ) : ( profiles.map((p) => ( {p.isDefault === true ? : " "} {String(p.name)} {p.label ? {String(p.label)} : null} )) )} app key: {data.hasAppApiKey === true ? "✓" : "not set"} ); } function MessageView({ data }: { data: Rec }) { const { message, ...rest } = data; return ( ✓ {String(message)} {Object.keys(rest).length > 0 ? : null} ); } function SearchView({ data }: { data: Rec }) { const boards = Array.isArray(data.boards) ? data.boards : []; const cards = Array.isArray(data.cards) ? data.cards : []; return ( {boards.length > 0 ? ( 0 ? 1 : 0}> Boards {boards.filter(isBoard).map((b) => ( ))} ) : null} {cards.length > 0 ? ( Cards {cards.filter(isCard).map((c) => ( ))} ) : null} {boards.length === 0 && cards.length === 0 ? ( (no results) ) : null} ); } function GenericRow({ item }: { item: unknown }) { if (isRecord(item) && typeof item.name === "string") { return ( {item.name} {String(item.id ?? "")} ); } return {clip(JSON.stringify(item) ?? "", 100)}; } function KeyValue({ obj, dim = false }: { obj: Rec; dim?: boolean }) { return ( {Object.entries(obj).map(([key, value]) => ( {key}: ))} ); } function ValueText({ value, dim }: { value: unknown; dim: boolean }) { if (value === null || value === undefined) return ; if (typeof value === "boolean") { return {String(value)}; } if (typeof value === "string" || typeof value === "number") { return ( {String(value)} ); } return {clip(JSON.stringify(value) ?? "", 80)}; }