UNPKG

portio

Version:

A beautiful terminal UI for managing processes on network ports (Windows only)

137 lines (136 loc) 6.35 kB
import React, { useState, useEffect, useMemo } from 'react'; import { Box, Text, useStdout } from 'ink'; import chalk from 'chalk'; const stripAnsi = (str) => { return str.replace(/\x1B[[(?);]{0,2}(;?\d)*./g, ''); }; export const ProcessTable = ({ data, selectedIndex = -1 }) => { const { stdout } = useStdout(); const [viewportStart, setViewportStart] = useState(0); // Use stable terminal dimensions const terminalWidth = stdout?.columns || 80; const terminalHeight = stdout?.rows || 24; useEffect(() => { const availableHeight = Math.max(5, terminalHeight - 10); if (selectedIndex >= 0) { if (selectedIndex < viewportStart) { setViewportStart(selectedIndex); } else if (selectedIndex >= viewportStart + availableHeight) { setViewportStart(selectedIndex - availableHeight + 1); } } }, [selectedIndex, terminalHeight]); if (data.length === 0) { return null; } const columns = ['#', 'PID', 'Port', 'Process', 'Command']; const minWidths = { '#': 7, // Increased for checkbox 'PID': 6, 'Port': 5, 'Process': 10, 'Command': 20 }; const columnWidths = useMemo(() => { const columnWidths = {}; const bordersAndPadding = 4 + (columns.length - 1) * 3; let availableWidth = terminalWidth - bordersAndPadding; columns.forEach(col => { let maxWidth = col.length; data.forEach(row => { const cellValue = stripAnsi(row[col] || ''); maxWidth = Math.max(maxWidth, Math.min(cellValue.length, col === 'Command' ? 60 : 30)); }); columnWidths[col] = Math.max(minWidths[col] || 10, maxWidth); }); let totalWidth = Object.values(columnWidths).reduce((sum, width) => sum + width, 0); if (totalWidth > availableWidth) { const ratio = availableWidth / totalWidth; columns.forEach(col => { if (col === 'Command') { columnWidths[col] = Math.max(minWidths[col] || 20, Math.floor((columnWidths[col] || 20) * ratio * 1.5)); } else { columnWidths[col] = Math.max(minWidths[col] || 10, Math.floor((columnWidths[col] || 10) * ratio)); } }); totalWidth = Object.values(columnWidths).reduce((sum, width) => sum + width, 0); if (totalWidth < availableWidth) { columnWidths['Command'] = (columnWidths['Command'] || 20) + (availableWidth - totalWidth); } } return columnWidths; }, [data, terminalWidth]); const truncateText = (text, maxWidth) => { const stripped = stripAnsi(text); if (stripped.length <= maxWidth) { return text; } if (maxWidth < 3) { return text.substring(0, maxWidth); } return text.substring(0, maxWidth - 3) + '...'; }; const renderCell = (value, width) => { const truncated = truncateText(value, width); const actualLength = stripAnsi(truncated).length; const padding = ' '.repeat(Math.max(0, width - actualLength)); return truncated + padding; }; const renderRow = (row, _index, isHeader = false) => { return (React.createElement(Box, null, React.createElement(Text, { color: "cyan" }, "\u2502 "), columns.map((col, colIndex) => { const value = isHeader ? col : row[col] || ''; const width = columnWidths[col] || minWidths[col] || 10; const displayValue = isHeader ? chalk.bold.white(value) : value; const cellContent = renderCell(displayValue, width); return (React.createElement(React.Fragment, { key: col }, React.createElement(Text, null, cellContent), colIndex < columns.length - 1 ? (React.createElement(Text, { color: "cyan" }, " \u2502 ")) : (React.createElement(Text, { color: "cyan" }, " \u2502")))); }))); }; const createBorder = (left, mid, right, cross) => { return chalk.cyan(left + columns.map((col, index) => { const width = (columnWidths[col] || minWidths[col] || 10) + 2; return mid.repeat(width) + (index < columns.length - 1 ? cross : ''); }).join('') + right); }; const topBorder = createBorder('╭', '─', '╮', '┬'); const middleBorder = createBorder('├', '─', '┤', '┼'); const bottomBorder = createBorder('╰', '─', '╯', '┴'); const availableHeight = Math.max(5, terminalHeight - 10); const visibleData = data.slice(viewportStart, viewportStart + availableHeight); // Calculate table width for centering const tableWidth = Object.values(columnWidths).reduce((sum, width) => sum + width, 0) + 4 + (columns.length - 1) * 3; // borders and separators const leftPadding = Math.max(0, Math.floor((terminalWidth - tableWidth) / 2)); const paddingString = ' '.repeat(leftPadding); return (React.createElement(Box, { flexDirection: "column" }, React.createElement(Text, null, paddingString, topBorder), React.createElement(Box, null, React.createElement(Text, null, paddingString), renderRow(Object.fromEntries(columns.map(c => [c, c])), -1, true)), React.createElement(Text, null, paddingString, middleBorder), visibleData.map((row, index) => (React.createElement(React.Fragment, { key: viewportStart + index }, React.createElement(Box, null, React.createElement(Text, null, paddingString), renderRow(row, viewportStart + index))))), React.createElement(Text, null, paddingString, bottomBorder), data.length > availableHeight && (React.createElement(Box, { marginTop: 1, justifyContent: "center" }, React.createElement(Text, { color: "magenta" }, "\u25B6 [", viewportStart + 1, "-", Math.min(viewportStart + availableHeight, data.length), " of ", data.length, "]"))))); };