1001 lines
31 KiB
TypeScript
1001 lines
31 KiB
TypeScript
|
import { colorAttributes, rgbAnsi } from "components/apps/Terminal/color";
|
||
|
import { config, PI_ASCII } from "components/apps/Terminal/config";
|
||
|
import {
|
||
|
aliases,
|
||
|
autoComplete,
|
||
|
commands,
|
||
|
getFreeSpace,
|
||
|
getUptime,
|
||
|
help,
|
||
|
parseCommand,
|
||
|
printColor,
|
||
|
printTable,
|
||
|
unknownCommand,
|
||
|
} from "components/apps/Terminal/functions";
|
||
|
import loadWapm from "components/apps/Terminal/loadWapm";
|
||
|
import processGit from "components/apps/Terminal/processGit";
|
||
|
import { runPython } from "components/apps/Terminal/python";
|
||
|
import type {
|
||
|
CommandInterpreter,
|
||
|
LocalEcho,
|
||
|
} from "components/apps/Terminal/types";
|
||
|
import {
|
||
|
displayLicense,
|
||
|
displayVersion,
|
||
|
} from "components/apps/Terminal/useTerminal";
|
||
|
import extensions from "components/system/Files/FileEntry/extensions";
|
||
|
import {
|
||
|
getModifiedTime,
|
||
|
getProcessByFileExtension,
|
||
|
getShortcutInfo,
|
||
|
} from "components/system/Files/FileEntry/functions";
|
||
|
import { useFileSystem } from "contexts/fileSystem";
|
||
|
import { requestPermission, resetStorage } from "contexts/fileSystem/functions";
|
||
|
import { useProcesses } from "contexts/process";
|
||
|
import processDirectory from "contexts/process/directory";
|
||
|
import { useProcessesRef } from "hooks/useProcessesRef";
|
||
|
import { basename, dirname, extname, isAbsolute, join } from "path";
|
||
|
import { useCallback, useEffect, useRef } from "react";
|
||
|
import { useTheme } from "styled-components";
|
||
|
import type UAParser from "ua-parser-js";
|
||
|
import {
|
||
|
DEFAULT_LOCALE,
|
||
|
DESKTOP_PATH,
|
||
|
HIGH_PRIORITY_REQUEST,
|
||
|
isFileSystemMappingSupported,
|
||
|
MILLISECONDS_IN_SECOND,
|
||
|
PACKAGE_DATA,
|
||
|
SHORTCUT_EXTENSION,
|
||
|
} from "utils/constants";
|
||
|
import { transcode } from "utils/ffmpeg";
|
||
|
import { getTZOffsetISOString, loadFiles } from "utils/functions";
|
||
|
import { convert } from "utils/imagemagick";
|
||
|
import { getIpfsFileName, getIpfsResource } from "utils/ipfs";
|
||
|
import { fullSearch } from "utils/search";
|
||
|
import { convertSheet } from "utils/sheetjs";
|
||
|
import type { Terminal } from "xterm";
|
||
|
|
||
|
const COMMAND_NOT_SUPPORTED = "The system does not support the command.";
|
||
|
const FILE_NOT_FILE = "The system cannot find the file specified.";
|
||
|
const PATH_NOT_FOUND = "The system cannot find the path specified.";
|
||
|
const SYNTAX_ERROR = "The syntax of the command is incorrect.";
|
||
|
|
||
|
const { alias } = PACKAGE_DATA;
|
||
|
|
||
|
type WindowPerformance = Performance & {
|
||
|
memory: {
|
||
|
jsHeapSizeLimit: number;
|
||
|
totalJSHeapSize: number;
|
||
|
};
|
||
|
};
|
||
|
|
||
|
type IResultWithGPU = UAParser.IResult & { gpu: UAParser.IDevice };
|
||
|
|
||
|
declare global {
|
||
|
interface Window {
|
||
|
UAParser: UAParser;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const useCommandInterpreter = (
|
||
|
id: string,
|
||
|
cd: React.MutableRefObject<string>,
|
||
|
terminal?: Terminal,
|
||
|
localEcho?: LocalEcho
|
||
|
): React.MutableRefObject<CommandInterpreter> => {
|
||
|
const {
|
||
|
createPath,
|
||
|
deletePath,
|
||
|
exists,
|
||
|
fs,
|
||
|
lstat,
|
||
|
mapFs,
|
||
|
mkdirRecursive,
|
||
|
readdir,
|
||
|
readFile,
|
||
|
rename,
|
||
|
rootFs,
|
||
|
stat,
|
||
|
updateFolder,
|
||
|
} = useFileSystem();
|
||
|
const { closeWithTransition, open, title: changeTitle } = useProcesses();
|
||
|
const processesRef = useProcessesRef();
|
||
|
const { name: themeName } = useTheme();
|
||
|
const getFullPath = useCallback(
|
||
|
async (file: string, writePath?: string): Promise<string> => {
|
||
|
if (!file) return "";
|
||
|
|
||
|
if (file.startsWith("ipfs://")) {
|
||
|
const ipfsData = await getIpfsResource(file);
|
||
|
const ipfsFile = join(
|
||
|
DESKTOP_PATH,
|
||
|
await createPath(
|
||
|
await getIpfsFileName(file, ipfsData),
|
||
|
writePath || DESKTOP_PATH,
|
||
|
ipfsData
|
||
|
)
|
||
|
);
|
||
|
|
||
|
updateFolder(writePath || DESKTOP_PATH, basename(ipfsFile));
|
||
|
|
||
|
return ipfsFile;
|
||
|
}
|
||
|
|
||
|
return isAbsolute(file) ? file : join(cd.current, file);
|
||
|
},
|
||
|
[cd, createPath, updateFolder]
|
||
|
);
|
||
|
const colorOutput = useRef<string[]>([]);
|
||
|
const updateFile = useCallback(
|
||
|
(filePath: string, isDeleted = false): void => {
|
||
|
const dirPath = dirname(filePath);
|
||
|
|
||
|
if (isDeleted) {
|
||
|
updateFolder(dirPath, undefined, basename(filePath));
|
||
|
} else {
|
||
|
updateFolder(dirPath, basename(filePath));
|
||
|
}
|
||
|
},
|
||
|
[updateFolder]
|
||
|
);
|
||
|
const commandInterpreter = useCallback(
|
||
|
async (command = ""): Promise<string> => {
|
||
|
const [baseCommand = "", ...commandArgs] = parseCommand(command);
|
||
|
const lcBaseCommand = baseCommand.toLowerCase();
|
||
|
|
||
|
// eslint-disable-next-line sonarjs/max-switch-cases
|
||
|
switch (lcBaseCommand) {
|
||
|
case "cat":
|
||
|
case "type": {
|
||
|
const [file] = commandArgs;
|
||
|
|
||
|
if (file) {
|
||
|
const fullPath = await getFullPath(file);
|
||
|
|
||
|
if (await exists(fullPath)) {
|
||
|
if ((await lstat(fullPath)).isDirectory()) {
|
||
|
localEcho?.println("Access is denied.");
|
||
|
} else {
|
||
|
localEcho?.println((await readFile(fullPath)).toString());
|
||
|
}
|
||
|
} else {
|
||
|
localEcho?.println(PATH_NOT_FOUND);
|
||
|
}
|
||
|
} else {
|
||
|
localEcho?.println(SYNTAX_ERROR);
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
case "cd":
|
||
|
case "cd/":
|
||
|
case "cd.":
|
||
|
case "cd..":
|
||
|
case "chdir":
|
||
|
case "pwd": {
|
||
|
const [directory] =
|
||
|
lcBaseCommand.startsWith("cd") && lcBaseCommand.length > 2
|
||
|
? [lcBaseCommand.slice(2)]
|
||
|
: commandArgs;
|
||
|
|
||
|
if (directory && lcBaseCommand !== "pwd") {
|
||
|
const fullPath = await getFullPath(directory);
|
||
|
|
||
|
if (await exists(fullPath)) {
|
||
|
if (!(await lstat(fullPath)).isDirectory()) {
|
||
|
localEcho?.println("The directory name is invalid.");
|
||
|
} else if (cd.current !== fullPath && localEcho) {
|
||
|
// eslint-disable-next-line no-param-reassign
|
||
|
cd.current = fullPath;
|
||
|
}
|
||
|
} else {
|
||
|
localEcho?.println(PATH_NOT_FOUND);
|
||
|
}
|
||
|
} else {
|
||
|
localEcho?.println(cd.current);
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
case "color": {
|
||
|
const [r, g, b] = commandArgs;
|
||
|
|
||
|
if (r !== undefined && g !== undefined && b !== undefined) {
|
||
|
localEcho?.print(rgbAnsi(Number(r), Number(g), Number(b)));
|
||
|
} else {
|
||
|
const [[bg, fg] = []] = commandArgs;
|
||
|
const { rgb: bgRgb, name: bgName } =
|
||
|
colorAttributes[bg?.toUpperCase()] || {};
|
||
|
const { rgb: fgRgb, name: fgName } =
|
||
|
colorAttributes[fg?.toUpperCase()] || {};
|
||
|
|
||
|
if (bgRgb) {
|
||
|
const useAsBg = Boolean(fgRgb);
|
||
|
const bgAnsi = rgbAnsi(...bgRgb, useAsBg);
|
||
|
|
||
|
localEcho?.print(bgAnsi);
|
||
|
localEcho?.println(
|
||
|
`${useAsBg ? "Background" : "Foreground"}: ${bgName}`
|
||
|
);
|
||
|
colorOutput.current[0] = bgAnsi;
|
||
|
}
|
||
|
|
||
|
if (fgRgb) {
|
||
|
const fgAnsi = rgbAnsi(...fgRgb);
|
||
|
|
||
|
localEcho?.print(fgAnsi);
|
||
|
localEcho?.println(`Foreground: ${fgName}`);
|
||
|
colorOutput.current[1] = fgAnsi;
|
||
|
}
|
||
|
|
||
|
if (!fgRgb && !bgRgb) {
|
||
|
localEcho?.print("\u001B[0m");
|
||
|
colorOutput.current = [];
|
||
|
}
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
case "copy":
|
||
|
case "cp": {
|
||
|
const [source, destination] = commandArgs;
|
||
|
const fullSourcePath = await getFullPath(source);
|
||
|
|
||
|
if (await exists(fullSourcePath)) {
|
||
|
if (destination) {
|
||
|
const fullDestinationPath = await getFullPath(destination);
|
||
|
const dirName = dirname(fullDestinationPath);
|
||
|
|
||
|
updateFile(
|
||
|
join(
|
||
|
dirName,
|
||
|
await createPath(
|
||
|
basename(fullDestinationPath),
|
||
|
dirName,
|
||
|
await readFile(fullSourcePath)
|
||
|
)
|
||
|
)
|
||
|
);
|
||
|
localEcho?.println("\t1 file(s) copied.");
|
||
|
} else {
|
||
|
localEcho?.println("The file cannot be copied onto itself.");
|
||
|
localEcho?.println("\t0 file(s) copied.");
|
||
|
}
|
||
|
} else {
|
||
|
localEcho?.println(FILE_NOT_FILE);
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
case "clear":
|
||
|
case "cls":
|
||
|
terminal?.reset();
|
||
|
terminal?.write(`\u001Bc${colorOutput.current.join("")}`);
|
||
|
break;
|
||
|
case "date":
|
||
|
localEcho?.println(
|
||
|
`The current date is: ${getTZOffsetISOString().slice(0, 10)}`
|
||
|
);
|
||
|
break;
|
||
|
case "del":
|
||
|
case "erase":
|
||
|
case "rd":
|
||
|
case "rm":
|
||
|
case "rmdir": {
|
||
|
const [commandPath] = commandArgs;
|
||
|
|
||
|
if (commandPath) {
|
||
|
const fullPath = await getFullPath(commandPath);
|
||
|
|
||
|
if (await exists(fullPath)) {
|
||
|
await deletePath(fullPath);
|
||
|
updateFile(fullPath, true);
|
||
|
}
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
case "dir":
|
||
|
case "ls": {
|
||
|
const [directory = ""] = commandArgs;
|
||
|
const listDir = async (dirPath: string): Promise<void> => {
|
||
|
let totalSize = 0;
|
||
|
let fileCount = 0;
|
||
|
let directoryCount = 0;
|
||
|
let entries = await readdir(dirPath);
|
||
|
|
||
|
if (
|
||
|
entries.length === 0 &&
|
||
|
rootFs?.mntMap[dirPath]?.getName() === "FileSystemAccess"
|
||
|
) {
|
||
|
await requestPermission(dirPath);
|
||
|
entries = await readdir(dirPath);
|
||
|
}
|
||
|
|
||
|
const timeFormatter = new Intl.DateTimeFormat(DEFAULT_LOCALE, {
|
||
|
timeStyle: "short",
|
||
|
});
|
||
|
const entriesWithStats = await Promise.all(
|
||
|
entries
|
||
|
.filter(
|
||
|
(entry) =>
|
||
|
(!directory.startsWith("*") ||
|
||
|
entry.endsWith(directory.slice(1))) &&
|
||
|
(!directory.endsWith("*") ||
|
||
|
entry.startsWith(directory.slice(0, -1)))
|
||
|
)
|
||
|
.map(async (entry) => {
|
||
|
const filePath = join(dirPath, entry);
|
||
|
const fileStats = await stat(filePath);
|
||
|
const mDate = new Date(getModifiedTime(filePath, fileStats));
|
||
|
const date = mDate.toISOString().slice(0, 10);
|
||
|
const time = timeFormatter.format(mDate).padStart(8, "0");
|
||
|
const isDirectory = fileStats.isDirectory();
|
||
|
|
||
|
totalSize += isDirectory ? 0 : fileStats.size;
|
||
|
if (isDirectory) {
|
||
|
directoryCount += 1;
|
||
|
} else {
|
||
|
fileCount += 1;
|
||
|
}
|
||
|
|
||
|
return [
|
||
|
`${date} ${time}`,
|
||
|
isDirectory
|
||
|
? "<DIR> "
|
||
|
: fileStats.size.toLocaleString(),
|
||
|
entry,
|
||
|
];
|
||
|
})
|
||
|
);
|
||
|
localEcho?.println(` Directory of ${dirPath}`);
|
||
|
localEcho?.println("");
|
||
|
|
||
|
const fullSizeTerminal =
|
||
|
!localEcho?._termSize?.cols || localEcho?._termSize?.cols > 52;
|
||
|
|
||
|
printTable(
|
||
|
[
|
||
|
["Date", fullSizeTerminal ? 22 : 20],
|
||
|
[
|
||
|
"Type/Size",
|
||
|
fullSizeTerminal ? 15 : 13,
|
||
|
true,
|
||
|
(size) => (size === "-1" ? "" : size),
|
||
|
],
|
||
|
["Name", terminal?.cols ? terminal.cols - 40 : 30],
|
||
|
],
|
||
|
entriesWithStats,
|
||
|
localEcho,
|
||
|
true
|
||
|
);
|
||
|
localEcho?.println(
|
||
|
`\t\t${fileCount} File(s)\t${totalSize.toLocaleString()} bytes`
|
||
|
);
|
||
|
localEcho?.println(
|
||
|
`\t\t${directoryCount} Dir(s)${await getFreeSpace()}`
|
||
|
);
|
||
|
};
|
||
|
|
||
|
if (
|
||
|
directory &&
|
||
|
!directory.startsWith("*") &&
|
||
|
!directory.endsWith("*")
|
||
|
) {
|
||
|
const fullPath = await getFullPath(directory);
|
||
|
|
||
|
if (await exists(fullPath)) {
|
||
|
if ((await lstat(fullPath)).isDirectory()) {
|
||
|
await listDir(fullPath);
|
||
|
} else {
|
||
|
localEcho?.println(basename(fullPath));
|
||
|
}
|
||
|
} else {
|
||
|
localEcho?.println("File Not Found");
|
||
|
}
|
||
|
} else {
|
||
|
await listDir(cd.current);
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
case "echo":
|
||
|
localEcho?.println(command.slice(command.indexOf(" ") + 1));
|
||
|
break;
|
||
|
case "exit":
|
||
|
case "quit":
|
||
|
closeWithTransition(id);
|
||
|
break;
|
||
|
case "file": {
|
||
|
const [commandPath] = commandArgs;
|
||
|
|
||
|
if (commandPath) {
|
||
|
const fullPath = await getFullPath(commandPath);
|
||
|
|
||
|
if (await exists(fullPath)) {
|
||
|
const { fileTypeFromBuffer } = await import("file-type");
|
||
|
const { mime = "Unknown" } =
|
||
|
(await fileTypeFromBuffer(await readFile(fullPath))) || {};
|
||
|
|
||
|
localEcho?.println(`${commandPath}: ${mime}`);
|
||
|
}
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
case "find":
|
||
|
case "search": {
|
||
|
const results = await fullSearch(
|
||
|
commandArgs.join(" "),
|
||
|
readFile,
|
||
|
rootFs
|
||
|
);
|
||
|
results?.forEach(({ ref }) => localEcho?.println(ref));
|
||
|
break;
|
||
|
}
|
||
|
case "ffmpeg":
|
||
|
case "imagemagick": {
|
||
|
const [file, format] = commandArgs;
|
||
|
|
||
|
if (file && format) {
|
||
|
const fullPath = await getFullPath(file);
|
||
|
|
||
|
if (
|
||
|
(await exists(fullPath)) &&
|
||
|
!(await lstat(fullPath)).isDirectory()
|
||
|
) {
|
||
|
const convertOrTranscode =
|
||
|
lcBaseCommand === "ffmpeg" ? transcode : convert;
|
||
|
const [[newName, newData]] = await convertOrTranscode(
|
||
|
[[basename(fullPath), await readFile(fullPath)]],
|
||
|
format,
|
||
|
localEcho
|
||
|
);
|
||
|
|
||
|
if (newName && newData) {
|
||
|
const dirName = dirname(fullPath);
|
||
|
|
||
|
updateFile(
|
||
|
join(dirName, await createPath(newName, dirName, newData))
|
||
|
);
|
||
|
}
|
||
|
} else {
|
||
|
localEcho?.println(FILE_NOT_FILE);
|
||
|
}
|
||
|
} else {
|
||
|
localEcho?.println(SYNTAX_ERROR);
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
case "git":
|
||
|
case "isogit":
|
||
|
if (fs && localEcho) {
|
||
|
await processGit(
|
||
|
commandArgs,
|
||
|
cd.current,
|
||
|
localEcho,
|
||
|
fs,
|
||
|
updateFolder
|
||
|
);
|
||
|
}
|
||
|
break;
|
||
|
case "help": {
|
||
|
const [commandName] = commandArgs;
|
||
|
|
||
|
if (localEcho) {
|
||
|
const showAliases = commandName === "-a";
|
||
|
|
||
|
if (commandName && !showAliases) {
|
||
|
const helpCommand = commands[commandName]
|
||
|
? commandName
|
||
|
: Object.entries(aliases).find(
|
||
|
([, [baseCommandName]]) => baseCommandName === commandName
|
||
|
)?.[0];
|
||
|
|
||
|
if (helpCommand && commands[helpCommand]) {
|
||
|
localEcho.println(commands[helpCommand]);
|
||
|
} else {
|
||
|
localEcho.println(
|
||
|
"This command is not supported by the help utility."
|
||
|
);
|
||
|
}
|
||
|
} else {
|
||
|
help(localEcho, commands, showAliases ? aliases : undefined);
|
||
|
}
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
case "history":
|
||
|
localEcho?.history.entries.forEach((entry, index) =>
|
||
|
localEcho.println(`${(index + 1).toString().padStart(4)} ${entry}`)
|
||
|
);
|
||
|
break;
|
||
|
case "ipfs": {
|
||
|
const [commandName, cid] = commandArgs;
|
||
|
|
||
|
if (commandName === "get" && cid) {
|
||
|
await getFullPath(`ipfs://${cid}`, cd.current);
|
||
|
}
|
||
|
|
||
|
break;
|
||
|
}
|
||
|
case "ifconfig":
|
||
|
case "ipconfig":
|
||
|
case "whatsmyip": {
|
||
|
const cloudFlareIpTraceText =
|
||
|
(await (
|
||
|
await fetch("https://cloudflare.com/cdn-cgi/trace")
|
||
|
).text()) || "";
|
||
|
const { ip = "" } = Object.fromEntries(
|
||
|
cloudFlareIpTraceText
|
||
|
.trim()
|
||
|
.split("\n")
|
||
|
.map((entry) => entry.split("=")) || []
|
||
|
) as Record<string, string>;
|
||
|
const isValidIp = (possibleIp: string): boolean => {
|
||
|
const octets = possibleIp.split(".");
|
||
|
|
||
|
return (
|
||
|
octets.length === 4 &&
|
||
|
octets.map(Number).every((octet) => octet > 0 && octet < 256)
|
||
|
);
|
||
|
};
|
||
|
|
||
|
localEcho?.println("IP Configuration");
|
||
|
localEcho?.println("");
|
||
|
localEcho?.println(
|
||
|
` IPv4 Address. . . . . . . . . . . : ${
|
||
|
isValidIp(ip) ? ip : "Unknown"
|
||
|
}`
|
||
|
);
|
||
|
break;
|
||
|
}
|
||
|
case "kill":
|
||
|
case "taskkill": {
|
||
|
const [processName] = commandArgs;
|
||
|
|
||
|
if (processesRef.current[processName]) {
|
||
|
closeWithTransition(processName);
|
||
|
localEcho?.println(
|
||
|
`SUCCESS: Sent termination signal to the process "${processName}".`
|
||
|
);
|
||
|
} else {
|
||
|
localEcho?.println(
|
||
|
`ERROR: The process "${processName}" not found.`
|
||
|
);
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
case "license":
|
||
|
localEcho?.println(displayLicense);
|
||
|
break;
|
||
|
case "md":
|
||
|
case "mkdir": {
|
||
|
const [directory] = commandArgs;
|
||
|
|
||
|
if (directory) {
|
||
|
const fullPath = await getFullPath(directory);
|
||
|
|
||
|
await mkdirRecursive(fullPath);
|
||
|
updateFile(fullPath);
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
case "mount":
|
||
|
if (localEcho) {
|
||
|
if (isFileSystemMappingSupported()) {
|
||
|
try {
|
||
|
const mappedFolder = await mapFs(cd.current);
|
||
|
|
||
|
if (mappedFolder) {
|
||
|
const fullPath = join(cd.current, mappedFolder);
|
||
|
|
||
|
updateFolder(cd.current, mappedFolder);
|
||
|
|
||
|
// eslint-disable-next-line no-param-reassign
|
||
|
cd.current = fullPath;
|
||
|
}
|
||
|
} catch {
|
||
|
// Ignore failure to mount
|
||
|
}
|
||
|
} else {
|
||
|
localEcho?.println(COMMAND_NOT_SUPPORTED);
|
||
|
}
|
||
|
}
|
||
|
break;
|
||
|
case "move":
|
||
|
case "mv":
|
||
|
case "ren":
|
||
|
case "rename": {
|
||
|
const [source, destination] = commandArgs;
|
||
|
const fullSourcePath = await getFullPath(source);
|
||
|
|
||
|
if (await exists(fullSourcePath)) {
|
||
|
if (destination) {
|
||
|
let fullDestinationPath = await getFullPath(destination);
|
||
|
|
||
|
if (
|
||
|
["move", "mv"].includes(lcBaseCommand) &&
|
||
|
(await stat(fullDestinationPath)).isDirectory()
|
||
|
) {
|
||
|
fullDestinationPath = join(
|
||
|
fullDestinationPath,
|
||
|
basename(fullSourcePath)
|
||
|
);
|
||
|
}
|
||
|
|
||
|
await rename(fullSourcePath, fullDestinationPath);
|
||
|
updateFile(fullSourcePath, true);
|
||
|
updateFile(fullDestinationPath);
|
||
|
} else {
|
||
|
localEcho?.println(SYNTAX_ERROR);
|
||
|
}
|
||
|
} else {
|
||
|
localEcho?.println(FILE_NOT_FILE);
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
case "neofetch":
|
||
|
case "systeminfo": {
|
||
|
await loadFiles(["/Program Files/Xterm.js/ua-parser.js"]);
|
||
|
|
||
|
const {
|
||
|
browser,
|
||
|
cpu,
|
||
|
engine,
|
||
|
gpu,
|
||
|
os: hostOS,
|
||
|
} = (new window.UAParser().getResult() || {}) as IResultWithGPU;
|
||
|
const { cols, options } = terminal || {};
|
||
|
const userId = `public@${window.location.hostname}`;
|
||
|
const terminalFont = (options?.fontFamily || config.fontFamily)
|
||
|
?.split(", ")
|
||
|
.find((font) =>
|
||
|
document.fonts.check(
|
||
|
`${options?.fontSize || config.fontSize || 12}px ${font}`
|
||
|
)
|
||
|
);
|
||
|
const { quota = 0, usage = 0 } =
|
||
|
(await navigator.storage?.estimate?.()) || {};
|
||
|
const labelColor = 3;
|
||
|
const labelText = (text: string): string =>
|
||
|
`${rgbAnsi(...colorAttributes[labelColor].rgb)}${text}${
|
||
|
colorOutput.current?.[0] || rgbAnsi(...colorAttributes[7].rgb)
|
||
|
}`;
|
||
|
const output = [
|
||
|
userId,
|
||
|
Array.from({ length: userId.length }).fill("-").join(""),
|
||
|
`OS: ${alias} ${displayVersion()}`,
|
||
|
];
|
||
|
|
||
|
if (hostOS?.name) {
|
||
|
output.push(
|
||
|
`Host: ${hostOS.name}${
|
||
|
hostOS?.version ? ` ${hostOS.version}` : ""
|
||
|
}${cpu?.architecture ? ` ${cpu?.architecture}` : ""}`
|
||
|
);
|
||
|
}
|
||
|
|
||
|
if (browser?.name) {
|
||
|
output.push(
|
||
|
`Kernel: ${browser.name}${
|
||
|
browser?.version ? ` ${browser.version}` : ""
|
||
|
}${engine?.name ? ` (${engine.name})` : ""}`
|
||
|
);
|
||
|
}
|
||
|
|
||
|
output.push(
|
||
|
`Uptime: ${getUptime(true)}`,
|
||
|
`Packages: ${Object.keys(processDirectory).length}`,
|
||
|
`Resolution: ${window.screen.width}x${window.screen.height}`,
|
||
|
`Theme: ${themeName}`
|
||
|
);
|
||
|
|
||
|
if (terminalFont) {
|
||
|
output.push(`Terminal Font: ${terminalFont}`);
|
||
|
}
|
||
|
|
||
|
if (gpu?.vendor) {
|
||
|
output.push(
|
||
|
`GPU: ${gpu.vendor}${gpu?.model ? ` ${gpu.model}` : ""}`
|
||
|
);
|
||
|
} else if (gpu?.model) {
|
||
|
output.push(`GPU: ${gpu.model}`);
|
||
|
}
|
||
|
|
||
|
if (window.performance && "memory" in window.performance) {
|
||
|
output.push(
|
||
|
`Memory: ${(
|
||
|
(window.performance as WindowPerformance).memory
|
||
|
.totalJSHeapSize /
|
||
|
1024 /
|
||
|
1024
|
||
|
).toFixed(0)}MB / ${(
|
||
|
(window.performance as WindowPerformance).memory
|
||
|
.jsHeapSizeLimit /
|
||
|
1024 /
|
||
|
1024
|
||
|
).toFixed(0)}MB`
|
||
|
);
|
||
|
}
|
||
|
|
||
|
if (quota) {
|
||
|
output.push(
|
||
|
`Disk (/): ${(usage / 1024 / 1024 / 1024).toFixed(0)}G / ${(
|
||
|
quota /
|
||
|
1024 /
|
||
|
1024 /
|
||
|
1024
|
||
|
).toFixed(0)}G (${((usage / quota) * 100).toFixed(2)}%)`
|
||
|
);
|
||
|
}
|
||
|
|
||
|
const longestLineLength = output.reduce(
|
||
|
(max, line) => Math.max(max, line.length),
|
||
|
0
|
||
|
);
|
||
|
const maxCols = cols || config.cols || 70;
|
||
|
|
||
|
if (localEcho) {
|
||
|
const longestLine = PI_ASCII[0].length + longestLineLength;
|
||
|
const imgPadding = Math.max(Math.min(maxCols - longestLine, 3), 1);
|
||
|
|
||
|
output.push(
|
||
|
"\n",
|
||
|
[0, 4, 2, 6, 1, 5, 3, 7]
|
||
|
.map((color) => printColor(color, colorOutput.current))
|
||
|
.join(""),
|
||
|
[8, "C", "A", "E", 9, "D", "B", "F"]
|
||
|
.map((color) => printColor(color, colorOutput.current))
|
||
|
.join("")
|
||
|
);
|
||
|
PI_ASCII.forEach((imgLine, lineIndex) => {
|
||
|
let outputLine = output[lineIndex] || "";
|
||
|
|
||
|
if (lineIndex === 0) {
|
||
|
const [user, system] = outputLine.split("@");
|
||
|
|
||
|
outputLine = `${labelText(user)}@${labelText(system)}`;
|
||
|
} else {
|
||
|
const [label, info] = outputLine.split(":");
|
||
|
|
||
|
if (info) {
|
||
|
outputLine = `${labelText(label)}:${info}`;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
localEcho.println(
|
||
|
`${labelText(imgLine)}${
|
||
|
outputLine.padStart(outputLine.length + imgPadding, " ") || ""
|
||
|
}`
|
||
|
);
|
||
|
});
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
case "sheep":
|
||
|
case "esheep": {
|
||
|
const { default: spawnSheep } = await import("utils/spawnSheep");
|
||
|
let [count = 1, duration = 0] = commandArgs;
|
||
|
|
||
|
if (!Number.isNaN(count) && !Number.isNaN(duration)) {
|
||
|
count = Number(count);
|
||
|
duration = Number(duration);
|
||
|
|
||
|
if (count > 1) {
|
||
|
await spawnSheep();
|
||
|
count -= 1;
|
||
|
}
|
||
|
|
||
|
const maxDuration =
|
||
|
(duration || (count > 1 ? 1 : 0)) * MILLISECONDS_IN_SECOND;
|
||
|
|
||
|
Array.from({ length: count })
|
||
|
.fill(0)
|
||
|
.map(() => Math.floor(Math.random() * maxDuration))
|
||
|
.forEach((delay) => setTimeout(spawnSheep, delay));
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
case "ps":
|
||
|
case "tasklist":
|
||
|
printTable(
|
||
|
[
|
||
|
["PID", 30],
|
||
|
["Title", 25],
|
||
|
],
|
||
|
Object.entries(processesRef.current).map(([pid, { title }]) => [
|
||
|
pid,
|
||
|
title,
|
||
|
]),
|
||
|
localEcho
|
||
|
);
|
||
|
break;
|
||
|
case "py":
|
||
|
case "python":
|
||
|
if (localEcho) {
|
||
|
const [file] = commandArgs;
|
||
|
const fullSourcePath = await getFullPath(file);
|
||
|
|
||
|
if (await exists(fullSourcePath)) {
|
||
|
const code = await readFile(fullSourcePath);
|
||
|
|
||
|
await runPython(code.toString(), localEcho);
|
||
|
} else {
|
||
|
await runPython(
|
||
|
command.slice(command.indexOf(" ") + 1),
|
||
|
localEcho
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
break;
|
||
|
case "logout":
|
||
|
case "restart":
|
||
|
case "shutdown":
|
||
|
resetStorage(rootFs).finally(() => window.location.reload());
|
||
|
break;
|
||
|
case "time":
|
||
|
localEcho?.println(
|
||
|
`The current time is: ${getTZOffsetISOString().slice(11, 22)}`
|
||
|
);
|
||
|
break;
|
||
|
case "title":
|
||
|
changeTitle(id, command.slice(command.indexOf(" ") + 1));
|
||
|
break;
|
||
|
case "touch": {
|
||
|
const [file] = commandArgs;
|
||
|
|
||
|
if (file) {
|
||
|
const fullPath = await getFullPath(file);
|
||
|
const dirName = dirname(fullPath);
|
||
|
|
||
|
updateFile(
|
||
|
join(
|
||
|
dirName,
|
||
|
await createPath(basename(fullPath), dirName, Buffer.from(""))
|
||
|
)
|
||
|
);
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
case "uptime":
|
||
|
localEcho?.println(`Uptime: ${getUptime()}`);
|
||
|
break;
|
||
|
case "ver":
|
||
|
case "version":
|
||
|
localEcho?.println(displayVersion());
|
||
|
break;
|
||
|
case "wapm":
|
||
|
case "wax":
|
||
|
if (localEcho) await loadWapm(commandArgs, localEcho);
|
||
|
break;
|
||
|
case "weather":
|
||
|
case "wttr": {
|
||
|
const response = await fetch(
|
||
|
"https://wttr.in/?1nAF",
|
||
|
HIGH_PRIORITY_REQUEST
|
||
|
);
|
||
|
|
||
|
localEcho?.println(await response.text());
|
||
|
|
||
|
const [bgAnsi, fgAnsi] = colorOutput.current;
|
||
|
|
||
|
if (bgAnsi) localEcho?.print(bgAnsi);
|
||
|
if (fgAnsi) localEcho?.print(fgAnsi);
|
||
|
break;
|
||
|
}
|
||
|
case "whoami":
|
||
|
localEcho?.println(`${window.location.hostname}\\public`);
|
||
|
break;
|
||
|
case "xlsx": {
|
||
|
const [file, format = "xlsx"] = commandArgs;
|
||
|
|
||
|
if (file && format) {
|
||
|
const fullPath = await getFullPath(file);
|
||
|
|
||
|
if (
|
||
|
(await exists(fullPath)) &&
|
||
|
!(await lstat(fullPath)).isDirectory()
|
||
|
) {
|
||
|
const workBook = await convertSheet(
|
||
|
await readFile(fullPath),
|
||
|
format
|
||
|
);
|
||
|
const dirName = dirname(fullPath);
|
||
|
|
||
|
updateFile(
|
||
|
join(
|
||
|
dirName,
|
||
|
await createPath(
|
||
|
`${basename(file, extname(file))}.${format}`,
|
||
|
dirName,
|
||
|
Buffer.from(workBook)
|
||
|
)
|
||
|
)
|
||
|
);
|
||
|
} else {
|
||
|
localEcho?.println(FILE_NOT_FILE);
|
||
|
}
|
||
|
} else {
|
||
|
localEcho?.println(SYNTAX_ERROR);
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
default:
|
||
|
if (baseCommand) {
|
||
|
const pid = Object.keys(processDirectory).find(
|
||
|
(process) => process.toLowerCase() === lcBaseCommand
|
||
|
);
|
||
|
|
||
|
if (pid) {
|
||
|
const [file] = commandArgs;
|
||
|
const fullPath = await getFullPath(file);
|
||
|
|
||
|
open(pid, {
|
||
|
url:
|
||
|
file && fullPath && (await exists(fullPath)) ? fullPath : "",
|
||
|
});
|
||
|
} else if (await exists(baseCommand)) {
|
||
|
const fileExtension = extname(baseCommand).toLowerCase();
|
||
|
const { command: extCommand = "" } =
|
||
|
extensions[fileExtension] || {};
|
||
|
|
||
|
if (extCommand) {
|
||
|
await commandInterpreter(`${extCommand} ${baseCommand}`);
|
||
|
} else {
|
||
|
let basePid = "";
|
||
|
let baseUrl = baseCommand;
|
||
|
|
||
|
if (fileExtension === SHORTCUT_EXTENSION) {
|
||
|
({ pid: basePid, url: baseUrl } = getShortcutInfo(
|
||
|
await readFile(baseCommand)
|
||
|
));
|
||
|
} else {
|
||
|
basePid = getProcessByFileExtension(fileExtension);
|
||
|
}
|
||
|
|
||
|
if (basePid) open(basePid, { url: baseUrl });
|
||
|
}
|
||
|
} else {
|
||
|
localEcho?.println(unknownCommand(baseCommand));
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (localEcho) {
|
||
|
readdir(cd.current).then((files) => autoComplete(files, localEcho));
|
||
|
}
|
||
|
|
||
|
return cd.current;
|
||
|
},
|
||
|
[
|
||
|
cd,
|
||
|
changeTitle,
|
||
|
closeWithTransition,
|
||
|
createPath,
|
||
|
deletePath,
|
||
|
exists,
|
||
|
fs,
|
||
|
getFullPath,
|
||
|
id,
|
||
|
localEcho,
|
||
|
lstat,
|
||
|
mapFs,
|
||
|
mkdirRecursive,
|
||
|
open,
|
||
|
processesRef,
|
||
|
readFile,
|
||
|
readdir,
|
||
|
rename,
|
||
|
rootFs,
|
||
|
stat,
|
||
|
terminal,
|
||
|
themeName,
|
||
|
updateFile,
|
||
|
updateFolder,
|
||
|
]
|
||
|
);
|
||
|
const commandInterpreterRef = useRef<CommandInterpreter>(commandInterpreter);
|
||
|
|
||
|
useEffect(() => {
|
||
|
commandInterpreterRef.current = commandInterpreter;
|
||
|
}, [commandInterpreter]);
|
||
|
|
||
|
return commandInterpreterRef;
|
||
|
};
|
||
|
|
||
|
export default useCommandInterpreter;
|