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, terminal?: Terminal, localEcho?: LocalEcho ): React.MutableRefObject => { 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 => { 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([]); 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 => { 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 => { 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 ? " " : 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; 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); useEffect(() => { commandInterpreterRef.current = commandInterpreter; }, [commandInterpreter]); return commandInterpreterRef; }; export default useCommandInterpreter;