bitburner-src/src/Terminal/commands/ls.tsx
Aleksei Bezrodnov 1af01401d9
UI: Correct behavior of "back" button on faction augs page
Plus router refactoring
2023-06-26 04:24:37 -04:00

233 lines
7.8 KiB
TypeScript

import React from "react";
import { Theme } from "@mui/material/styles";
import type { TextFilePath } from "../../Paths/TextFilePath";
import type { ContractFilePath } from "../../Paths/ContractFilePath";
import type { ProgramFilePath } from "../../Paths/ProgramFilePath";
import type { ContentFilePath } from "../../Paths/ContentFile";
import type { ScriptFilePath } from "../../Paths/ScriptFilePath";
import createStyles from "@mui/styles/createStyles";
import makeStyles from "@mui/styles/makeStyles";
import { BaseServer } from "../../Server/BaseServer";
import { Router } from "../../ui/GameRoot";
import { Page } from "../../ui/Router";
import { Terminal } from "../../Terminal";
import libarg from "arg";
import { showLiterature } from "../../Literature/LiteratureHelpers";
import { showMessage } from "../../Message/MessageHelpers";
import { FilePath, combinePath, removeDirectoryFromPath } from "../../Paths/FilePath";
import {
Directory,
directoryExistsOnServer,
getFirstDirectoryInPath,
resolveDirectory,
root,
} from "../../Paths/Directory";
import { isMember } from "../../utils/EnumHelper";
export function ls(args: (string | number | boolean)[], server: BaseServer): void {
interface LSFlags {
["-l"]: boolean;
["--grep"]: string;
}
let flags: LSFlags;
try {
flags = libarg(
{
"-l": Boolean,
"--grep": String,
"-g": "--grep",
},
{ argv: args },
);
} catch (e) {
// catch passing only -g / --grep with no string to use as the search
incorrectUsage();
return;
}
const filter = flags["--grep"] ?? "";
const numArgs = args.length;
function incorrectUsage(): void {
Terminal.error("Incorrect usage of ls command. Usage: ls [dir] [-l] [-g, --grep pattern]");
}
if (numArgs > 4) {
return incorrectUsage();
}
let baseDirectory = Terminal.currDir;
// Parse first argument which should be a directory.
if (args[0] && typeof args[0] == "string" && !args[0].startsWith("-")) {
const directory = resolveDirectory(args[0], args[0].startsWith("/") ? root : Terminal.currDir);
if (directory !== null && directoryExistsOnServer(directory, server)) {
baseDirectory = directory;
} else return incorrectUsage();
}
// Display all programs and scripts
const allPrograms: ProgramFilePath[] = [];
const allScripts: ScriptFilePath[] = [];
const allTextFiles: TextFilePath[] = [];
const allContracts: ContractFilePath[] = [];
const allMessages: FilePath[] = [];
const folders: Directory[] = [];
function handlePath(path: FilePath, dest: FilePath[]): void {
// This parses out any files not in the starting directory.
const parsedPath = removeDirectoryFromPath(baseDirectory, path);
if (!parsedPath) return;
if (!parsedPath.includes(filter)) return;
// Check if there's a directory in the parsed path, if so we need to add the folder and not the file.
const firstParentDir = getFirstDirectoryInPath(parsedPath);
if (firstParentDir) {
if (!firstParentDir.includes(filter) || folders.includes(firstParentDir)) return;
folders.push(firstParentDir);
return;
}
dest.push(parsedPath);
}
// Get all of the programs and scripts on the machine into one temporary array
// Type assertions that programs and msg/lit are filepaths are safe due to checks in
// Program, Message, and Literature constructors
for (const program of server.programs) handlePath(program as FilePath, allPrograms);
for (const scriptFilename of server.scripts.keys()) handlePath(scriptFilename, allScripts);
for (const txtFilename of server.textFiles.keys()) handlePath(txtFilename, allTextFiles);
for (const contract of server.contracts) handlePath(contract.fn, allContracts);
for (const msgOrLit of server.messages) handlePath(msgOrLit as FilePath, allMessages);
// Sort the files/folders alphabetically then print each
allPrograms.sort();
allScripts.sort();
allTextFiles.sort();
allContracts.sort();
allMessages.sort();
folders.sort();
function SegmentGrid(props: { colSize: string; children: React.ReactChild[] }): React.ReactElement {
const classes = makeStyles({
segmentGrid: {
display: "grid",
gridTemplateColumns: "repeat(auto-fill, var(--colSize))",
},
})();
const style = { ["--colSize"]: props.colSize } as React.CSSProperties;
return (
<span style={style} className={classes.segmentGrid}>
{props.children}
</span>
);
}
function ClickableScriptLink(props: { path: ScriptFilePath }): React.ReactElement {
const classes = makeStyles((theme: Theme) =>
createStyles({
link: {
cursor: "pointer",
textDecorationLine: "underline",
color: theme.palette.warning.main,
},
}),
)();
const fullPath = combinePath(baseDirectory, props.path);
function onClick() {
const code = server.scripts.get(fullPath)?.content ?? "";
const files = new Map<ContentFilePath, string>();
files.set(fullPath, code);
Router.toPage(Page.ScriptEditor, { files });
}
return (
<span>
<span className={classes.link} onClick={onClick}>
{props.path}
</span>
</span>
);
}
function ClickableMessageLink(props: { path: FilePath }): React.ReactElement {
const classes = makeStyles({
link: {
cursor: "pointer",
textDecorationLine: "underline",
},
})();
function onClick(): void {
if (!server.isConnectedTo) {
return Terminal.error(`File is not on this server, connect to ${server.hostname} and try again`);
}
// Message and lit files are always in root, no need to combine path with base directory
if (isMember("MessageFilename", props.path)) {
showMessage(props.path);
} else if (isMember("LiteratureName", props.path)) {
showLiterature(props.path);
}
}
return (
<span>
<span className={classes.link} onClick={onClick}>
{props.path}
</span>
</span>
);
}
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 | FileType.TextFile;
segments: string[];
}
| { type: FileType.Message; segments: FilePath[] }
| { type: FileType.Script; segments: ScriptFilePath[] };
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:
segmentElements = segments.map((segment) => <ClickableScriptLink key={segment} path={segment} />);
break;
default:
segmentElements = segments.map((segment) => <span key={segment}>{segment}</span>);
}
Terminal.printRaw(<SegmentGrid colSize={colSize}>{segmentElements}</SegmentGrid>);
}
const groups: FileGroup[] = [
{ type: FileType.Folder, segments: folders },
{ type: FileType.Message, segments: allMessages },
{ type: FileType.TextFile, segments: allTextFiles },
{ type: FileType.Program, segments: allPrograms },
{ type: FileType.Contract, segments: allContracts },
{ type: FileType.Script, segments: allScripts },
];
for (const group of groups) {
if (group.segments.length > 0) postSegments(group, flags);
}
}