mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2026-04-16 06:18:42 +02:00
TERMINAL: Add RAM usage and detailed file size to ls -l output (#2135)
This commit is contained in:
@@ -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.",
|
||||
" ",
|
||||
|
||||
@@ -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[] = [
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user