import { type Transaction, type TransactionStatusCode, type TransactionsRecord, transactionStatusLabels, } from "@olenbetong/appframe-updater"; export const DEFAULT_TABLE_SEPARATOR = " │ "; export const STATUS_DIRTY_INDICATOR = "*"; const STATUS_COLUMN_MIN_WIDTH = (() => { let labels = Object.values(transactionStatusLabels) as string[]; let longest = labels.reduce((max, label) => Math.max(max, label.length), 0); return Math.max("Status".length, longest + STATUS_DIRTY_INDICATOR.length); })(); export interface TableColumn { key: keyof Row; header: string; min: number; max: number; weight: number; shrinkPriority?: number; } export interface EditorTransactionRow { Namespace: string; Name: string; CreatedBy: string; Status: string; LastError?: string; [key: string]: string | undefined; } export function sanitizeCellValue(value: unknown) { let s = String(value ?? ""); s = s.replace(/[\r\n]+/g, " ").replace(/\t/g, " "); s = s.replace(/\s{2,}/g, " ").trim(); return s; } export function truncateCellValue(value: string, width: number) { if (width <= 0) { return ""; } if (value.length <= width) { return value.padEnd(width, " "); } if (width <= 1) { return "…".slice(0, width); } return `${value.slice(0, width - 1)}…`; } export function calculateColumnWidths>({ columns, rows, totalWidth, separator = DEFAULT_TABLE_SEPARATOR, }: { columns: TableColumn[]; rows: Row[]; totalWidth: number; separator?: string; }) { let effectiveWidth = Math.max(totalWidth, 0); let separatorWidth = separator.length * Math.max(columns.length - 1, 0); let widths = columns.map((column) => column.header.length); for (let index = 0; index < columns.length; index++) { let column = columns[index]; let maxContent = column.header.length; for (let row of rows) { let value = sanitizeCellValue(row[column.key]); if (value.length > maxContent) { maxContent = value.length; } } widths[index] = Math.min(Math.max(maxContent, column.min), column.max); } let currentWidth = widths.reduce((sum, width) => sum + width, 0) + separatorWidth; if (currentWidth > effectiveWidth) { let ordered = [...columns] .map((column, index) => ({ column, index })) .sort((a, z) => { let aPriority = a.column.shrinkPriority ?? Number.POSITIVE_INFINITY; let zPriority = z.column.shrinkPriority ?? Number.POSITIVE_INFINITY; return aPriority - zPriority; }); while (currentWidth > effectiveWidth) { let shrunk = false; for (let { column, index } of ordered) { let newWidth = Math.max(column.min, widths[index] - 1); if (newWidth < widths[index]) { widths[index] = newWidth; currentWidth = widths.reduce((sum, width) => sum + width, 0) + separatorWidth; shrunk = true; if (currentWidth <= effectiveWidth) { break; } } } if (!shrunk) { break; } } } else if (currentWidth < effectiveWidth) { let extra = effectiveWidth - currentWidth; let totalWeight = columns.reduce((sum, column) => sum + column.weight, 0); if (extra > 0 && totalWeight > 0) { while (extra > 0) { let allocated = false; for (let index = 0; index < columns.length && extra > 0; index++) { let column = columns[index]; if (column.weight === 0) { continue; } let increment = Math.max(1, Math.floor((extra * column.weight) / totalWeight)); if (widths[index] + increment > column.max) { increment = column.max - widths[index]; } if (increment > 0) { widths[index] += increment; extra -= increment; allocated = true; } } if (!allocated) { break; } } } } return widths; } export function formatRowLines>({ row, columns, widths, separator = DEFAULT_TABLE_SEPARATOR, }: { row: Row; columns: TableColumn[]; widths: number[]; separator?: string; }) { let parts = columns.map((column, index) => { let width = widths[index]; let value = sanitizeCellValue(row[column.key]); return truncateCellValue(value, width); }); let line = parts.join(separator); if (line.length === 0) { return [columns.map((_, index) => "".padEnd(widths[index], " ")).join(separator)]; } return [line]; } export function addViewportEllipsis(line: string) { if (line.length === 0) { return line; } let trimmed = line.trimEnd(); if (trimmed.length >= line.length) { trimmed = trimmed.slice(0, Math.max(line.length - 1, 0)); } let ellipsisLine = `${trimmed}${line.length > 0 ? "…" : ""}`; if (ellipsisLine.length < line.length) { ellipsisLine = ellipsisLine.padEnd(line.length, " "); } else if (ellipsisLine.length > line.length) { ellipsisLine = ellipsisLine.slice(0, line.length); } return ellipsisLine; } export function createMessageLine(message: string, tableWidth: number, lineWidth: number, prefix = "") { let content = truncateCellValue(message, tableWidth); let line = `${prefix}${content}`; if (line.length < lineWidth) { line = line.padEnd(lineWidth, " "); } else if (line.length > lineWidth) { line = line.slice(0, lineWidth); } return line; } export function createStatusLine(message: string, stats: string, lineWidth: number, prefix = "") { if (lineWidth <= 0) { return ""; } let contentWidth = Math.max(lineWidth - prefix.length, 0); let right = stats.length > 0 ? truncateCellValue(stats, contentWidth).trimEnd() : ""; let availableForLeft = Math.max(contentWidth - right.length, 0); let left = message.length > 0 ? truncateCellValue(message, availableForLeft).trimEnd() : ""; let padding = Math.max(contentWidth - (left.length + right.length), 0); let content = `${left}${" ".repeat(padding)}${right}`; let line = `${prefix}${content}`; if (line.length < lineWidth) { line = line.padEnd(lineWidth, " "); } else if (line.length > lineWidth) { line = line.slice(0, lineWidth); } return line; } export function createTransactionColumns(showError: boolean): TableColumn[] { let columns: TableColumn[] = [ { key: "Namespace", header: "Namespace", min: 10, max: 24, weight: 0, shrinkPriority: 3 }, { key: "Name", header: "Name", min: 32, max: 40, weight: 1, shrinkPriority: 1 }, { key: "CreatedBy", header: "CreatedBy", min: 13, max: 20, weight: 0, shrinkPriority: 4 }, { key: "Status", header: "Status", min: STATUS_COLUMN_MIN_WIDTH, max: Math.max(STATUS_COLUMN_MIN_WIDTH, 16), weight: 0, shrinkPriority: 5, }, ]; if (showError) { columns.push({ key: "LastError", header: "Error", min: 20, max: 120, weight: 2, shrinkPriority: 0, }); } return columns; } export function mapTransactionsToRows(transactions: Transaction[], showError: boolean): EditorTransactionRow[] { let rows: EditorTransactionRow[] = transactions.map((transaction) => { let statusCode = transaction.Status as TransactionStatusCode; let statusLabel = transactionStatusLabels[statusCode]; return { Namespace: transaction.Namespace ?? "", Name: transaction.Name ?? "", CreatedBy: transaction.CreatedBy ?? "", Status: statusLabel ?? String(transaction.Status ?? ""), LastError: transaction.LastError ?? "", }; }); if (!showError) { rows = rows.map((row) => ({ Namespace: row.Namespace, Name: row.Name, CreatedBy: row.CreatedBy, Status: row.Status, })); } return rows; } export function formatTransactionsPreview(records: TransactionsRecord[]) { return records .map((record, index) => { let identifier = record.PrimKey ?? record.ID ?? `#${index + 1}`; let header = `Transaction ${index + 1} (${identifier})`; let body = JSON.stringify(record, null, 2); return `${header}\n${body}`; }) .join("\n\n"); } export function resolveEditorCommand() { if (process.platform === "win32") { let command = process.env.COMSPEC && process.env.COMSPEC.length > 0 ? process.env.COMSPEC : "cmd.exe"; return { command, args: ["/c", "edit"], label: "edit" }; } return { command: "nano", args: [], label: "nano" }; }