TERMINAL: Add RAM usage and detailed file size to ls -l output (#2135)

This commit is contained in:
HansLuft778
2025-06-02 11:23:22 +02:00
committed by GitHub
parent b17b0d710b
commit 67017c5782
4 changed files with 229 additions and 64 deletions

View File

@@ -27,7 +27,7 @@ export const TerminalHelpText: string[] = [
" ipaddr Displays the IP address of the machine",
" kill [script/pid] [args...] Stops the specified script on the current server ",
" killall Stops all running scripts on the current machine",
" ls [dir] [--grep pattern] Displays all files on the machine",
" ls [dir] [-l] [-h] [-g pattern] Displays all files on the machine",
" lscpu Displays the number of CPU cores on the machine",
" mem [script] [-t n] Displays the amount of RAM required to run the script",
" mv [src] [dest] Move/rename a text or script file",
@@ -336,14 +336,16 @@ export const HelpTexts: Record<string, string[]> = {
],
killall: ["Usage: killall", " ", "Kills all scripts on the current server."],
ls: [
"Usage: ls [dir] [-l] [--grep pattern]",
"Usage: ls [dir] [-l] [-h] [-g, --grep pattern]",
" ",
"The ls command, with no arguments, prints all files and directories on the current server's directory to the Terminal screen. ",
"The files will be displayed in alphabetical order. ",
" ",
"The 'dir' optional parameter can be used to display files/directories in another directory.",
" ",
"The '-l' optional parameter allows you to force each item onto a single line.",
"The '-l' optional parameter allows you to force each item onto a single line, displaying two columns for the script's RAM usage and filesize.",
" ",
"The '-h' optional parameter allows you to display the filesize from '-l' in human readable format (e.g. KB, MB, GB) instead of bytes.",
" ",
"The '--grep pattern' optional parameter can be used to only display files whose filenames match the specified pattern.",
" ",

View File

@@ -25,10 +25,31 @@ import {
} from "../../Paths/Directory";
import { isMember } from "../../utils/EnumHelper";
import { Settings } from "../../Settings/Settings";
import { formatBytes, formatRam } from "../../ui/formatNumber";
export function ls(args: (string | number | boolean)[], server: BaseServer): void {
enum FileType {
Folder,
Message,
TextFile,
Program,
Contract,
Script,
}
type FileGroup =
| {
// Types that are not clickable only need to be string[]
type: FileType.Folder | FileType.Program | FileType.Contract;
segments: string[];
}
| { type: FileType.Message; segments: FilePath[] }
| { type: FileType.Script; segments: ScriptFilePath[] }
| { type: FileType.TextFile; segments: TextFilePath[] };
interface LSFlags {
["-l"]: boolean;
["-h"]: boolean;
["--grep"]: string;
}
let flags: LSFlags;
@@ -37,6 +58,7 @@ export function ls(args: (string | number | boolean)[], server: BaseServer): voi
flags = libarg(
{
"-l": Boolean,
"-h": Boolean,
"--grep": String,
"-g": "--grep",
},
@@ -51,10 +73,10 @@ export function ls(args: (string | number | boolean)[], server: BaseServer): voi
const numArgs = args.length;
function incorrectUsage(): void {
Terminal.error("Incorrect usage of ls command. Usage: ls [dir] [-l] [-g, --grep pattern]");
Terminal.error("Incorrect usage of ls command. Usage: ls [dir] [-l] [-h] [-g, --grep pattern]");
}
if (numArgs > 4) {
if (numArgs > 5) {
return incorrectUsage();
}
@@ -109,6 +131,74 @@ export function ls(args: (string | number | boolean)[], server: BaseServer): voi
allMessages.sort();
folders.sort();
let maxSizeStrLength = 0;
let maxRamStrLength = 0;
if (flags["-l"]) {
// Collect all items to calculate max string lengths
const allDisplayableItems: { path: FilePath | Directory; type: FileType }[] = [];
folders.forEach((p) => allDisplayableItems.push({ path: p, type: FileType.Folder }));
allMessages.forEach((p) => allDisplayableItems.push({ path: p, type: FileType.Message }));
allTextFiles.forEach((p) => allDisplayableItems.push({ path: p, type: FileType.TextFile }));
allScripts.forEach((p) => allDisplayableItems.push({ path: p, type: FileType.Script }));
allPrograms.forEach((p) => allDisplayableItems.push({ path: p, type: FileType.Program }));
allContracts.forEach((p) => allDisplayableItems.push({ path: p, type: FileType.Contract }));
for (const item of allDisplayableItems) {
const { ramDisplay, sizeDisplay } = getItemNumericData(item.path, item.type);
if (sizeDisplay.length > maxSizeStrLength) maxSizeStrLength = sizeDisplay.length;
if (ramDisplay.length > maxRamStrLength) maxRamStrLength = ramDisplay.length;
}
}
function getItemNameElement(relativePath: string, fileType: FileType): React.ReactElement {
switch (fileType) {
case FileType.Folder:
return <span style={{ color: "cyan" }}>{relativePath}</span>;
case FileType.Message:
return <ClickableMessageLink path={relativePath as FilePath} />;
case FileType.TextFile:
case FileType.Script:
return <ClickableContentFileLink path={relativePath as ScriptFilePath | TextFilePath} />;
case FileType.Program:
case FileType.Contract:
default:
return <span>{relativePath}</span>;
}
}
function getItemNumericData(relativePath: string, fileType: FileType): { ramDisplay: string; sizeDisplay: string } {
let sizeDisplay = "-";
const fullPath =
fileType === FileType.Message || relativePath.startsWith("/")
? (relativePath as FilePath)
: combinePath(baseDirectory, relativePath as FilePath);
// Determine file size
let contentBytes = 0;
if (fileType === FileType.TextFile) {
const file = server.textFiles.get(fullPath as TextFilePath);
contentBytes = file?.content ? new TextEncoder().encode(file.content).length : 0;
} else {
// Script
const file = server.scripts.get(fullPath as ScriptFilePath);
contentBytes = file?.content ? new TextEncoder().encode(file.content).length : 0;
}
if (flags["-l"] && flags["-h"]) {
sizeDisplay = formatBytes(contentBytes);
} else {
sizeDisplay = `${contentBytes}`;
}
// Determine RAM usage
let ramDisplay = "-";
if (fileType === FileType.Script) {
const file = server.scripts.get(fullPath as ScriptFilePath);
const ramUsage = file?.getRamUsage(server.scripts);
ramDisplay = ramUsage ? formatRam(ramUsage) : "NaN";
}
return { ramDisplay, sizeDisplay };
}
function SegmentGrid(props: { colSize: string; children: React.ReactChild[] }): React.ReactElement {
const { classes } = makeStyles()({
segmentGrid: {
@@ -123,6 +213,7 @@ export function ls(args: (string | number | boolean)[], server: BaseServer): voi
</span>
);
}
function ClickableContentFileLink(props: { path: ScriptFilePath | TextFilePath }): React.ReactElement {
const { classes } = makeStyles()((theme: Theme) => ({
link: {
@@ -180,49 +271,63 @@ export function ls(args: (string | number | boolean)[], server: BaseServer): voi
);
}
enum FileType {
Folder,
Message,
TextFile,
Program,
Contract,
Script,
function LongListItem(props: {
children: React.ReactNode;
sizeInfo: string;
ramInfo: string;
maxSizeStrLengthCalculated: number;
maxRamStrLengthCalculated: number;
}): React.ReactElement {
const sizeColumnWidth = props.maxSizeStrLengthCalculated > 0 ? `${props.maxSizeStrLengthCalculated}ch` : "auto";
const ramColumnWidth = props.maxRamStrLengthCalculated > 0 ? `${props.maxRamStrLengthCalculated}ch` : "auto";
return (
<div
style={{
display: "grid",
gridTemplateColumns: `${ramColumnWidth} ${sizeColumnWidth} 1fr`,
alignItems: "baseline",
gap: "1em",
}}
>
<span style={{ color: Settings.theme.secondary, whiteSpace: "nowrap", textAlign: "right" }}>
{props.ramInfo}
</span>
<span style={{ color: Settings.theme.secondary, whiteSpace: "nowrap", textAlign: "right" }}>
{props.sizeInfo}
</span>
<span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{props.children}</span>
</div>
);
}
type FileGroup =
| {
// Types that are not clickable only need to be string[]
type: FileType.Folder | FileType.Program | FileType.Contract;
segments: string[];
}
| { type: FileType.Message; segments: FilePath[] }
| { type: FileType.Script; segments: ScriptFilePath[] }
| { type: FileType.TextFile; segments: TextFilePath[] };
function postSegments({ type, segments }: FileGroup, flags: LSFlags): void {
let segmentElements: React.ReactElement[];
const colSize = flags["-l"]
? "100%"
: Math.ceil(Math.max(...segments.map((segment) => segment.length)) * 0.7) + "em";
switch (type) {
case FileType.Folder:
segmentElements = segments.map((segment) => (
<span key={segment} style={{ color: "cyan" }}>
{segment}
</span>
));
break;
case FileType.Message:
segmentElements = segments.map((segment) => <ClickableMessageLink key={segment} path={segment} />);
break;
case FileType.Script:
case FileType.TextFile:
segmentElements = segments.map((segment) => <ClickableContentFileLink key={segment} path={segment} />);
break;
default:
segmentElements = segments.map((segment) => <span key={segment}>{segment}</span>);
if (segments.length === 0) return;
// print file based on mode
if (flags["-l"]) {
for (const segmentPath of segments) {
const { ramDisplay, sizeDisplay } = getItemNumericData(segmentPath, type);
const nameElement = getItemNameElement(segmentPath, type);
Terminal.printRaw(
<LongListItem
key={segmentPath.toString()}
sizeInfo={sizeDisplay}
ramInfo={ramDisplay}
maxSizeStrLengthCalculated={maxSizeStrLength}
maxRamStrLengthCalculated={maxRamStrLength}
>
{nameElement}
</LongListItem>,
);
}
} else {
const segmentElements = segments.map((segmentPath) => {
const nameElement = getItemNameElement(segmentPath, type);
return React.cloneElement(nameElement, { key: segmentPath.toString() });
});
const colSize = Math.ceil(Math.max(...segments.map((segment) => segment.length)) * 0.7) + "em";
Terminal.printRaw(<SegmentGrid colSize={colSize}>{segmentElements}</SegmentGrid>);
}
Terminal.printRaw(<SegmentGrid colSize={colSize}>{segmentElements}</SegmentGrid>);
}
const groups: FileGroup[] = [

View File

@@ -7,18 +7,18 @@ const numberSuffixList = ["", "k", "m", "b", "t", "q", "Q", "s", "S", "o", "n"];
const numberExpList = numberSuffixList.map((_, i) => parseFloat(`1e${i * 3}`));
// Ram suffixes
const ramLog1000Suffixes = ["GB", "TB", "PB", "EB"];
const ramLog1024Suffixes = ["GiB", "TiB", "PiB", "EiB"];
const decByteSuffixes = ["B", "KB", "MB", "GB", "TB", "PB", "EB"];
const binByteSuffixes = ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"];
// Items that get initialized in the initializer function.
let digitFormats = {} as Record<number, Intl.NumberFormat | undefined>,
percentFormats = {} as Record<number, Intl.NumberFormat | undefined>,
basicFormatter: Intl.NumberFormat,
exponentialFormatter: Intl.NumberFormat,
ramSuffixList: string[],
ramExpList: number[],
ramLogFn: (n: number) => number,
ramLogDivisor: number;
unitSuffixes: string[],
unitExpList: number[],
unitLogFn: (n: number) => number,
unitLogDivisor: number;
/** Event to be emitted when changing number display settings. */
export const FormatsNeedToChange = new EventEmitter();
@@ -33,12 +33,12 @@ FormatsNeedToChange.subscribe(() => {
percentFormats = {};
exponentialFormatter = makeFormatter(3, { notation: Settings.useEngineeringNotation ? "engineering" : "scientific" });
basicFormatter = new Intl.NumberFormat([Settings.Locale, "en"], { useGrouping: !Settings.hideThousandsSeparator });
[ramSuffixList, ramLogFn, ramLogDivisor] = Settings.UseIEC60027_2
[unitSuffixes, unitLogFn, unitLogDivisor] = Settings.UseIEC60027_2
? // log2 of 1024 is 10 as divisor for log base 1024
[ramLog1024Suffixes, Math.log2, 10]
[binByteSuffixes, Math.log2, 10]
: // log10 of 1000 is 3 as divisor for log base 1000
[ramLog1000Suffixes, Math.log10, 3];
ramExpList = ramSuffixList.map((_, i) => (Settings.UseIEC60027_2 ? 1024 : 1000) ** i);
[decByteSuffixes, Math.log10, 3];
unitExpList = unitSuffixes.map((_, i) => (Settings.UseIEC60027_2 ? 1024 : 1000) ** i);
// Emit a FormatsHaveChanged event so any static content that uses formats can be regenerated.
FormatsHaveChanged.emit();
@@ -67,26 +67,37 @@ function getFormatter(
return (formatList[fractionalDigits] = makeFormatter(fractionalDigits, options));
}
/** Display standard byte formatting. */
export function formatBytes(n: number, fractionalDigits = 1): string {
return formatSize(n, fractionalDigits, 0);
}
/** Display standard ram formatting. */
export function formatRam(n: number, fractionalDigits = 2) {
// NaN does not get formatted
if (Number.isNaN(n)) return `NaN${ramSuffixList[0]}`;
export function formatRam(n: number, fractionalDigits = 2): string {
return formatSize(n, fractionalDigits, 3);
}
function formatSize(n: number, fractionalDigits = 2, unitOffset = 3) {
const base = Settings.UseIEC60027_2 ? 1024 : 1000;
const nAbs = Math.abs(n);
// Special handling for Infinities
if (nAbs === Infinity) return `${n < 0 ? "-∞" : ""}${ramSuffixList.at(-1)}`;
// Special handling for NaN, Infinities and zero
if (Number.isNaN(n)) return `NaN${unitSuffixes[0 + unitOffset]}`;
if (nAbs === Infinity) return `${n < 0 ? "-∞" : "∞"}${unitSuffixes.at(-1)}`;
// Early return if using first suffix.
if (nAbs < 1000) return getFormatter(fractionalDigits).format(n) + ramSuffixList[0];
if (nAbs < base) return getFormatter(fractionalDigits).format(n) + unitSuffixes[unitOffset];
// Ram always uses a suffix and never goes to exponential
const suffixIndex = Math.min(Math.floor(ramLogFn(nAbs) / ramLogDivisor), ramSuffixList.length - 1);
n /= ramExpList[suffixIndex];
// convert input units to bytes
let nBytes = n * base ** unitOffset;
const suffixIndex = Math.min(Math.floor(unitLogFn(nBytes) / unitLogDivisor), unitSuffixes.length - 1);
nBytes /= unitExpList[suffixIndex];
/* Not really concerned with 1000-rounding or 1024-rounding for ram due to the actual values ram gets displayed at.
If display of e.g. 1,000.00GB instead of 1.00TB for 999.995GB, or 1,024.00GiB instead of 1.00TiB for 1,023.995GiB
becomes an actual issue we can add smart rounding, but ram values like that really don't happen ingame so it's
probably not worth the performance overhead to check and correct these. */
return getFormatter(fractionalDigits).format(n) + ramSuffixList[suffixIndex];
return getFormatter(fractionalDigits).format(nBytes) + unitSuffixes[suffixIndex];
}
function formatExponential(n: number) {

View File

@@ -6,6 +6,7 @@ import {
FormatsNeedToChange,
formatNumber,
formatRam,
formatBytes,
} from "../../../src/ui/formatNumber";
describe("Suffix rounding test", () => {
@@ -187,6 +188,7 @@ describe("Ram formatting", () => {
Settings.hideTrailingDecimalZeros = false;
FormatsNeedToChange.emit();
// Base unit for ram is GB.
expect(formatRam(0)).toEqual("0.00GB");
expect(formatRam(1)).toEqual("1.00GB");
expect(formatRam(1e3)).toEqual("1.00TB");
expect(formatRam(1024)).toEqual("1.02TB");
@@ -194,6 +196,9 @@ describe("Ram formatting", () => {
expect(formatRam(1048576)).toEqual("1.05PB");
expect(formatRam(1e9)).toEqual("1.00EB");
expect(formatRam(1073741824)).toEqual("1.07EB");
expect(formatRam(NaN)).toEqual("NaNGB");
expect(formatRam(Infinity)).toEqual("∞EB");
expect(formatRam(-Infinity)).toEqual("-∞EB");
});
test("With GiB mode", () => {
// Initial settings
@@ -201,6 +206,7 @@ describe("Ram formatting", () => {
Settings.hideTrailingDecimalZeros = false;
FormatsNeedToChange.emit();
// Base unit for ram is now GiB.
expect(formatRam(0)).toEqual("0.00GiB");
expect(formatRam(1)).toEqual("1.00GiB");
expect(formatRam(1e3)).toEqual("1,000.00GiB");
expect(formatRam(1024)).toEqual("1.00TiB");
@@ -208,5 +214,46 @@ describe("Ram formatting", () => {
expect(formatRam(1048576)).toEqual("1.00PiB");
expect(formatRam(1e9)).toEqual("953.67PiB");
expect(formatRam(1073741824)).toEqual("1.00EiB");
expect(formatRam(NaN)).toEqual("NaNGiB");
expect(formatRam(Infinity)).toEqual("∞EiB");
expect(formatRam(-Infinity)).toEqual("-∞EiB");
});
});
describe("Filesize formatting", () => {
test("With default GB mode", () => {
// Initial settings
Settings.UseIEC60027_2 = false;
Settings.hideTrailingDecimalZeros = false;
FormatsNeedToChange.emit();
// Base unit for ram is GB.
expect(formatBytes(0)).toEqual("0.0B");
expect(formatBytes(1)).toEqual("1.0B");
expect(formatBytes(1e3)).toEqual("1.0KB");
expect(formatBytes(1024)).toEqual("1.0KB");
expect(formatBytes(1e6)).toEqual("1.0MB");
expect(formatBytes(1048576)).toEqual("1.0MB");
expect(formatBytes(1e9)).toEqual("1.0GB");
expect(formatBytes(1073741824)).toEqual("1.1GB");
expect(formatBytes(NaN)).toEqual("NaNB");
expect(formatBytes(Infinity)).toEqual("∞EB");
expect(formatBytes(-Infinity)).toEqual("-∞EB");
});
test("With GiB mode", () => {
// Initial settings
Settings.UseIEC60027_2 = true;
Settings.hideTrailingDecimalZeros = false;
FormatsNeedToChange.emit();
// Base unit for ram is now GiB.
expect(formatBytes(0)).toEqual("0.0B");
expect(formatBytes(1)).toEqual("1.0B");
expect(formatBytes(1e3)).toEqual("1,000.0B");
expect(formatBytes(1024)).toEqual("1.0KiB");
expect(formatBytes(1e6)).toEqual("976.6KiB");
expect(formatBytes(1048576)).toEqual("1.0MiB");
expect(formatBytes(1e9)).toEqual("953.7MiB");
expect(formatBytes(1073741824)).toEqual("1.0GiB");
expect(formatBytes(NaN)).toEqual("NaNB");
expect(formatBytes(Infinity)).toEqual("∞EiB");
expect(formatBytes(-Infinity)).toEqual("-∞EiB");
});
});