/** * List command for Torm CLI. * * Lists all torrents with their status, progress, and speed. * * @module cli/commands/list */ import React, { useEffect, useState } from 'react'; import { render, Text, Box } from 'ink'; import { Torrent, TorrentState } from '../../engine/types.js'; import { getDaemonClient } from '../../daemon/index.js'; import { formatBytes, formatSpeed, formatProgress, truncateText, getColoredStatus, formatTableHeader, formatTableRow, TableColumn, infoMessage, errorMessage, } from '../utils/output.js'; // ============================================================================= // Types // ============================================================================= export interface ListCommandOptions { /** Filter by state */ state?: TorrentState; /** Show detailed output */ verbose?: boolean; } interface ListResultProps { torrents: Torrent[]; error: string | null; loading: boolean; verbose: boolean; } // ============================================================================= // Constants // ============================================================================= const TABLE_COLUMNS: TableColumn[] = [ { header: 'Name', width: 30, align: 'left' }, { header: 'Size', width: 10, align: 'right' }, { header: 'Progress', width: 8, align: 'right' }, { header: 'Speed', width: 10, align: 'right' }, { header: 'Status', width: 12, align: 'left' }, ]; // ============================================================================= // Components // ============================================================================= /** * Map torrent state to Ink color */ function getStateColor(state: TorrentState): string { switch (state) { case TorrentState.DOWNLOADING: return 'blue'; case TorrentState.SEEDING: return 'green'; case TorrentState.PAUSED: return 'yellow'; case TorrentState.ERROR: return 'red'; case TorrentState.CHECKING: return 'cyan'; case TorrentState.QUEUED: return 'gray'; default: return 'white'; } } /** * Get display name for torrent state */ function getStateName(state: TorrentState): string { switch (state) { case TorrentState.DOWNLOADING: return 'Downloading'; case TorrentState.SEEDING: return 'Seeding'; case TorrentState.PAUSED: return 'Paused'; case TorrentState.ERROR: return 'Error'; case TorrentState.CHECKING: return 'Checking'; case TorrentState.QUEUED: return 'Queued'; default: return 'Unknown'; } } /** * Get relevant speed for a torrent based on its state */ function getRelevantSpeed(torrent: Torrent): number { if (torrent.state === TorrentState.SEEDING) { return torrent.uploadSpeed; } return torrent.downloadSpeed; } /** * Torrent row component */ const TorrentRow: React.FC<{ torrent: Torrent; index: number }> = ({ torrent, index, }) => { const stateColor = getStateColor(torrent.state); const stateName = getStateName(torrent.state); const speed = getRelevantSpeed(torrent); const progressStr = formatProgress(torrent.progress); return ( {(index + 1).toString().padStart(3)}. {truncateText(torrent.name, 30)} {formatBytes(torrent.size)} {progressStr} {formatSpeed(speed)} {stateName} ); }; /** * Table header component */ const TableHeader: React.FC = () => ( {' #'} Name Size Progress Speed {' '} Status ); /** * Separator line */ const Separator: React.FC = () => {'-'.repeat(84)}; /** * Component to display the list of torrents */ const ListResult: React.FC = ({ torrents, error, loading, verbose: _verbose, }) => { if (loading) { return ( Loading torrents... ); } if (error) { return ( [ERROR] {error} ); } if (torrents.length === 0) { return ( [INFO] No torrents found ); } return ( {torrents.map((torrent, index) => ( ))} {torrents.length} torrent{torrents.length !== 1 ? 's' : ''} total ); }; // ============================================================================= // Main List Function // ============================================================================= /** * Execute the list command (non-interactive). * * @param options - Command options */ export async function executeList( options: ListCommandOptions = {} ): Promise { const { state, verbose: _verbose = false } = options; let client; try { // Connect to daemon (starts it if not running) client = await getDaemonClient(); let torrents = await client.getTorrents(); // Filter by state if specified if (state) { torrents = torrents.filter((t) => t.state === state); } if (torrents.length === 0) { console.log(infoMessage('No torrents found')); client.disconnect(); return; } // Print header console.log(formatTableHeader(TABLE_COLUMNS)); // Print each torrent for (const torrent of torrents) { const speed = getRelevantSpeed(torrent); const values = [ truncateText(torrent.name, 30), formatBytes(torrent.size), formatProgress(torrent.progress), formatSpeed(speed), getColoredStatus(torrent.state), ]; console.log(formatTableRow(values, TABLE_COLUMNS)); } console.log(); console.log( `${torrents.length} torrent${torrents.length !== 1 ? 's' : ''} total` ); client.disconnect(); } catch (err) { const message = err instanceof Error ? err.message : String(err); console.error(errorMessage(message)); if (client) { client.disconnect(); } process.exit(1); } } /** * List command component using Ink for rendering */ export function ListCommand({ state, verbose = false, }: ListCommandOptions): React.ReactElement { const [torrents, setTorrents] = useState([]); const [error, setError] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { const doList = async () => { let client; try { // Connect to daemon (starts it if not running) client = await getDaemonClient(); let allTorrents = await client.getTorrents(); // Filter by state if specified if (state) { allTorrents = allTorrents.filter((t) => t.state === state); } setTorrents(allTorrents); client.disconnect(); } catch (err) { const message = err instanceof Error ? err.message : String(err); setError(message); if (client) { client.disconnect(); } } finally { setLoading(false); } }; doList(); }, [state]); return ( ); } /** * Run the list command with Ink rendering */ export function runList(options: ListCommandOptions = {}): void { const { waitUntilExit } = render(); waitUntilExit().then(() => { process.exit(0); }); } export default executeList;