/**
* 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;