import React from 'react'; import { Box, Text } from 'ink'; import type { TrackerInfo, TrackerStatus } from '../../engine/types.js'; import { colors, borders } from '../theme/index.js'; import { formatRelativeTime, formatTimeUntil, truncateText, } from '../utils/format.js'; export interface TrackerListProps { /** Array of trackers to display */ trackers: TrackerInfo[]; } /** * Column widths for tracker list display */ const COLUMN_WIDTHS = { url: 35, status: 12, peers: 6, seeds: 6, leeches: 6, lastAnnounce: 10, nextAnnounce: 10, } as const; /** * Tracker status display configuration */ const STATUS_DISPLAY: Record = { idle: { color: colors.muted, label: 'Idle' }, announcing: { color: colors.warning, label: 'Announcing' }, working: { color: colors.success, label: 'Working' }, error: { color: colors.error, label: 'Error' }, }; /** * Get display configuration for a tracker status */ function getStatusDisplay(status: TrackerStatus | string): { color: string; label: string; } { const statusKey = typeof status === 'string' ? status.toLowerCase() : String(status).toLowerCase(); return ( STATUS_DISPLAY[statusKey] || { color: colors.muted, label: String(status) } ); } /** * Truncate tracker URL for display * Shows domain and path, removes protocol */ function formatTrackerUrl(url: string, maxLength: number): string { try { const parsed = new URL(url); const display = parsed.host + parsed.pathname; return truncateText(display, maxLength); } catch { return truncateText(url, maxLength); } } /** * Header row for tracker list */ const TrackerListHeader: React.FC = () => { return ( Tracker Status Peers Seeds Leech Last Next {borders.horizontal.repeat( COLUMN_WIDTHS.url + COLUMN_WIDTHS.status + COLUMN_WIDTHS.peers + COLUMN_WIDTHS.seeds + COLUMN_WIDTHS.leeches + COLUMN_WIDTHS.lastAnnounce + COLUMN_WIDTHS.nextAnnounce )} ); }; /** * Single tracker row component */ const TrackerRow: React.FC<{ tracker: TrackerInfo }> = ({ tracker }) => { const statusInfo = getStatusDisplay(tracker.status); const url = formatTrackerUrl(tracker.url, COLUMN_WIDTHS.url); const lastAnnounce = formatRelativeTime(tracker.lastAnnounce); const nextAnnounce = formatTimeUntil(tracker.nextAnnounce); return ( {url.padEnd(COLUMN_WIDTHS.url)} {statusInfo.label.padEnd(COLUMN_WIDTHS.status)} 0 ? colors.success : colors.muted}> {tracker.peers.toString().padStart(COLUMN_WIDTHS.peers)} 0 ? colors.success : colors.muted}> {tracker.seeds.toString().padStart(COLUMN_WIDTHS.seeds)} 0 ? colors.primary : colors.muted}> {tracker.leeches.toString().padStart(COLUMN_WIDTHS.leeches)} {lastAnnounce.padStart(COLUMN_WIDTHS.lastAnnounce)} {nextAnnounce.padStart(COLUMN_WIDTHS.nextAnnounce)} ); }; /** * Error row showing tracker error message */ const TrackerErrorRow: React.FC<{ message: string }> = ({ message }) => { return ( {'\u2514'} {'\u2500'} {truncateText(message, 70)} ); }; /** * Empty state when no trackers */ const EmptyState: React.FC = () => { return ( No trackers configured ); }; /** * TrackerList component for displaying tracker status * * Shows a table of trackers with their URL, status, peer/seed counts, * and announce timing information. * * @example * * * // Output: * // Tracker Status Peers Seeds Leech Last Next * // ────────────────────────────────────────────────────────────────────────────────────── * // tracker.example.com/announce Working 45 12 33 2m ago in 25m * // backup.tracker.org:6969/announce Error 0 0 0 15m ago in 30m * // └─ Connection refused */ export const TrackerList: React.FC = ({ trackers }) => { if (trackers.length === 0) { return ; } return ( {trackers.map((tracker) => ( {tracker.errorMessage && ( )} ))} ); }; export default TrackerList;