portio
Version:
A beautiful terminal UI for managing processes on network ports (Windows only)
137 lines (136 loc) • 6.35 kB
JavaScript
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,
"]")))));
};