Finished rudimentary filesystem implementation for Terminal

This commit is contained in:
danielyxie
2019-04-09 23:07:12 -07:00
parent 3241945452
commit 3ce2e83dd8
15 changed files with 1232 additions and 690 deletions

View File

@ -16,6 +16,84 @@ the terminal and enter::
nano .fconf
.. _terminal_filesystem:
Filesystem (Directories)
------------------------
The Terminal contains a **very** basic filesystem that allows you to store and
organize your files into different directories. Note that this is **not** a true
filesystem implementation. Instead, it is done almost entirely using string manipulation.
For this reason, many of the nice & useful features you'd find in a real
filesystem do not exist.
Here are the Terminal commands you'll commonly use when dealing with the filesystem.
* :ref:`ls_terminal_command`
* :ref:`cd_terminal_command`
* :ref:`mv_terminal_command`
Directories
^^^^^^^^^^^
In order to create a directory, simply name a file using a full absolute Linux-style path::
/scripts/myScript.js
This will automatically create a "directory" called :code:`scripts`. This will also work
for subdirectories::
/scripts/hacking/helpers/myHelperScripts.script
Files in the root directory do not need to begin with a forward slash::
thisIsAFileInTheRootDirectory.txt
Note that there is no way to manually create or remove directories. The creation and
deletion of directories is automatically handled as you name/rename/delete
files.
Absolute vs Relative Paths
^^^^^^^^^^^^^^^^^^^^^^^^^^
Many Terminal commands accept absolute both absolute and relative paths for specifying a
file.
An absolute path specifies the location of the file from the root directory (/).
Any path that begins with the forward slash is an absolute path::
$ nano /scripts/myScript.js
$ cat /serverList.txt
A relative path specifies the location of the file relative to the current working directory.
Any path that does **not** begin with a forward slash is a relative path. Note that the
Linux-style dot symbols will work for relative paths::
. (a single dot) - represents the current directory
.. (two dots) - represents the parent directory
$ cd ..
$ nano ../scripts/myScript.js
$ nano ../../helper.js
Netscript
^^^^^^^^^
Note that in order to reference a file, :ref:`netscript` functions require the
**full** absolute file path. For example
.. code:: javascript
run("/scripts/hacking/helpers.myHelperScripts.script");
rm("/logs/myHackingLogs.txt");
rm("thisIsAFileInTheRootDirectory.txt");
.. note:: A full file path **must** begin with a forward slash (/) if that file
is not in the root directory.
Missing Features
^^^^^^^^^^^^^^^^
Terminal/Filesystem features that are not yet implemented:
* Tab autocompletion does not work with relative paths
Commands
--------
@ -98,6 +176,25 @@ Display a message (.msg), literature (.lit), or text (.txt) file::
$ cat foo.lit
$ cat servers.txt
.. _cd_terminal_command:
cd
^^
$ cd [dir]
Change to the specified directory.
See :ref:`terminal_filesystem` for details on directories.
Note that this command works even for directories that don't exist. If you change
to a directory that doesn't exist, it will not be created. A directory is only created
once there is a file in it::
$ cd scripts/hacking
$ cd /logs
$ cd ..
check
^^^^^
@ -234,27 +331,35 @@ killall
Kills all scripts on the current server.
.. _ls_terminal_command:
ls
^^
$ ls [| grep pattern]
$ ls [dir] [| grep pattern]
Prints files on the current server to the Terminal screen.
Prints files and directories on the current server to the Terminal screen.
If this command is run with no arguments, then it prints all files on the current
server to the Terminal screen. The files will be displayed in alphabetical
order.
If this command is run with no arguments, then it prints all files and directories on the current
server to the Terminal screen. Directories will be printed first in alphabetical order,
followed by the files (also in alphabetical order).
The '| grep pattern' is an optional parameter that can be used to only display files
whose filenames match the specified pattern. For example, if you wanted to only display
files with the .script extension, you could use::
The :code:`dir` optional parameter allows you to specify the directory for which to display
files.
The :code:`| grep pattern` optional parameter allows you to only display files and directories
with a certain pattern in their names.
Examples::
// List files/directories with the '.script' extension in the current directory
$ ls | grep .script
Alternatively, if you wanted to display all files with the word *purchase* in the filename,
you could use::
// List files/directories with the '.js' extension in the root directory
$ ls / | grep .js
$ ls | grep purchase
// List files/directories with the word 'purchase' in the name, in the :code:`scripts` directory
$ ls scripts | grep purchase
lscpu
@ -282,6 +387,25 @@ The first example above will print the amount of RAM needed to run 'foo.script'
with a single thread. The second example above will print the amount of RAM needed
to run 'foo.script' with 50 threads.
.. _mv_terminal_command:
mv
^^
$ mv [source] [destination]
Move the source file to the specified destination in the filesystem.
See :ref:`terminal_filesystem` for more details about the Terminal's filesystem.
This command only works for scripts and text files (.txt). It cannot, however, be used
to convert from script to text file, or vice versa.
Note that this function can also be used to rename files.
Examples::
$ mv hacking.script scripts/hacking.script
$ mv myScript.js myOldScript.js
nano
^^^^

View File

@ -2097,12 +2097,6 @@ function displayAugmentationsContent(contentEl) {
innerText:"Purchased Augmentations",
}));
//Bladeburner text, once mechanic is unlocked
var bladeburnerText = "\n";
if (Player.bitNodeN === 6 || hasBladeburnerSF) {
bladeburnerText = "Bladeburner Progress\n\n";
}
contentEl.appendChild(createElement("pre", {
width:"70%", whiteSpace:"pre-wrap", display:"block",
innerText:"Below is a list of all Augmentations you have purchased but not yet installed. Click the button below to install them.\n" +
@ -2114,7 +2108,6 @@ function displayAugmentationsContent(contentEl) {
"Hacknet Nodes\n" +
"Faction/Company reputation\n" +
"Stocks\n" +
bladeburnerText +
"Installing Augmentations lets you start over with the perks and benefits granted by all " +
"of the Augmentations you have ever installed. Also, you will keep any scripts and RAM/Core upgrades " +
"on your home computer (but you will lose all programs besides NUKE.exe)."

View File

@ -274,6 +274,9 @@ export let CONSTANTS: IMap<any> = {
LatestUpdate:
`
v0.46.1
* Added a very rudimentary directory system to the Terminal
** Details here: https://bitburner.readthedocs.io/en/latest/basicgameplay/terminal.html#filesystem-directories
* Added numHashes(), hashCost(), and spendHashes() functions to the Netscript Hacknet Node API
* 'Generate Coding Contract' hash upgrade is now more expensive
* 'Generate Coding Contract' hash upgrade now generates the contract randomly on the server, rather than on home computer

View File

@ -98,9 +98,10 @@ export function createPurchaseServerPopup(ram: number, p: IPlayer) {
yesNoTxtInpBoxClose();
});
yesNoTxtInpBoxCreate("Would you like to purchase a new server with " + ram +
"GB of RAM for $" + numeralWrapper.formatMoney(cost) + "?<br><br>" +
"Please enter the server hostname below:<br>");
yesNoTxtInpBoxCreate(
`Would you like to purchase a new server with ${ram} GB of RAM for ` +
`${numeralWrapper.formatMoney(cost)}?<br><br>Please enter the server hostname below:<br>`
);
}
/**

View File

@ -70,7 +70,11 @@ function checkForMessagesToSend() {
}
} else if (jumper0 && !jumper0.recvd && Player.hacking_skill >= 25) {
sendMessage(jumper0);
Player.getHomeComputer().programs.push(Programs.Flight.name);
const flightName = Programs.Flight.name;
const homeComp = Player.getHomeComputer();
if (!homeComp.programs.includes(flightName)) {
homeComp.programs.push(flightName);
}
} else if (jumper1 && !jumper1.recvd && Player.hacking_skill >= 40) {
sendMessage(jumper1);
} else if (cybersecTest && !cybersecTest.recvd && Player.hacking_skill >= 50) {

View File

@ -2315,56 +2315,14 @@ function NetscriptFunctions(workerScript) {
if (ip == null || ip === "") {
ip = workerScript.serverIp;
}
var s = getServer(ip);
if (s == null) {
throw makeRuntimeRejectMsg(workerScript, `Invalid server specified for rm(): ${ip}`);
const s = safeGetServer(ip, "rm");
const status = s.removeFile(fn);
if (!status.res) {
workerScript.log(status.msg);
}
if (fn.endsWith(".exe")) {
for (var i = 0; i < s.programs.length; ++i) {
if (s.programs[i] === fn) {
s.programs.splice(i, 1);
return true;
}
}
} else if (isScriptFilename(fn)) {
for (var i = 0; i < s.scripts.length; ++i) {
if (s.scripts[i].filename === fn) {
//Check that the script isnt currently running
for (var j = 0; j < s.runningScripts.length; ++j) {
if (s.runningScripts[j].filename === fn) {
workerScript.scriptRef.log("Cannot delete a script that is currently running!");
return false;
}
}
s.scripts.splice(i, 1);
return true;
}
}
} else if (fn.endsWith(".lit")) {
for (var i = 0; i < s.messages.length; ++i) {
var f = s.messages[i];
if (!(f instanceof Message) && isString(f) && f === fn) {
s.messages.splice(i, 1);
return true;
}
}
} else if (fn.endsWith(".txt")) {
for (var i = 0; i < s.textFiles.length; ++i) {
if (s.textFiles[i].fn === fn) {
s.textFiles.splice(i, 1);
return true;
}
}
} else if (fn.endsWith(".cct")) {
for (var i = 0; i < s.contracts.length; ++i) {
if (s.contracts[i].fn === fn) {
s.contracts.splice(i, 1);
return true;
}
}
}
return false;
return status.res;
},
scriptRunning : function(scriptname, ip) {
if (workerScript.checkingRam) {

View File

@ -17,7 +17,7 @@ import { AllServers } from "../Server/AllServers";
import { processSingleServerGrowth } from "../Server/ServerHelpers";
import { Settings } from "../Settings/Settings";
import { EditorSetting } from "../Settings/SettingEnums";
import { isValidFilename } from "../Terminal/DirectoryHelpers";
import { isValidFilePath } from "../Terminal/DirectoryHelpers";
import {TextFile} from "../TextFile";
import {Page, routing} from "../ui/navigationTracking";
@ -248,7 +248,7 @@ function saveAndCloseScriptEditor() {
return;
}
if (filename !== ".fconf" && !isValidFilename(filename)) {
if (filename !== ".fconf" && !isValidFilePath(filename)) {
dialogBoxCreate("Script filename can contain only alphanumerics, hyphens, and underscores");
return;
}

View File

@ -6,6 +6,7 @@ import { Message } from "../Message/Message";
import { RunningScript } from "../Script/RunningScript";
import { Script } from "../Script/Script";
import { TextFile } from "../TextFile";
import { IReturnStatus } from "../types";
import { isScriptFilename } from "../Script/ScriptHelpersTS";
@ -123,6 +124,20 @@ export class BaseServer {
return null;
}
/**
* Returns boolean indicating whether the given script is running on this server
*/
isRunning(fn: string): boolean {
// Check that the script isnt currently running
for (const runningScriptObj of this.runningScripts) {
if (runningScriptObj.filename === fn) {
return true;
}
}
return false;
}
removeContract(contract: CodingContract) {
if (contract instanceof CodingContract) {
this.contracts = this.contracts.filter((c) => {
@ -135,6 +150,60 @@ export class BaseServer {
}
}
/**
* Remove a file from the server
* @param fn {string} Name of file to be deleted
* @returns {IReturnStatus} Return status object indicating whether or not file was deleted
*/
removeFile(fn: string): IReturnStatus {
if (fn.endsWith(".exe")) {
for (let i = 0; i < this.programs.length; ++i) {
if (this.programs[i] === fn) {
this.programs.splice(i, 1);
return { res: true };
}
}
} else if (isScriptFilename(fn)) {
for (let i = 0; i < this.scripts.length; ++i) {
if (this.scripts[i].filename === fn) {
if (this.isRunning(fn)) {
return {
res: false,
msg: "Cannot delete a script that is currently running!",
};
}
this.scripts.splice(i, 1);
return { res: true };
}
}
} else if (fn.endsWith(".lit")) {
for (let i = 0; i < this.messages.length; ++i) {
let f = this.messages[i];
if (typeof f === "string" && f === fn) {
this.messages.splice(i, 1);
return { res: true };
}
}
} else if (fn.endsWith(".txt")) {
for (let i = 0; i < this.textFiles.length; ++i) {
if (this.textFiles[i].fn === fn) {
this.textFiles.splice(i, 1);
return { res: true };
}
}
} else if (fn.endsWith(".cct")) {
for (let i = 0; i < this.contracts.length; ++i) {
if (this.contracts[i].fn === fn) {
this.contracts.splice(i, 1);
return { res: true };
}
}
}
return { res: false, msg: "No such file exists" };
}
/**
* Called when a script is run on this server.
* All this function does is add a RunningScript object to the

View File

@ -136,14 +136,6 @@ export class Server extends BaseServer {
this.minDifficulty = Math.max(1, this.minDifficulty);
}
/**
* Strengthens a server's security level (difficulty) by the specified amount
*/
fortify(amt: number): void {
this.hackDifficulty += amt;
this.capDifficulty();
}
/**
* Change this server's maximum money
* @param n - Value by which to change the server's maximum money
@ -157,6 +149,14 @@ export class Server extends BaseServer {
}
}
/**
* Strengthens a server's security level (difficulty) by the specified amount
*/
fortify(amt: number): void {
this.hackDifficulty += amt;
this.capDifficulty();
}
/**
* Lowers the server's security level (difficulty) by the specified amount)
*/

File diff suppressed because it is too large Load Diff

View File

@ -3,6 +3,29 @@
* These aren't real directories, they're more of a pseudo-directory implementation
*/
/**
* Removes leading forward slash ("/") from a string.
*/
export function removeLeadingSlash(s: string): string {
if (s.startsWith("/")) {
return s.slice(1);
}
return s;
}
/**
* Removes trailing forward slash ("/") from a string.
* Note that this will also remove the slash if it is the leading slash (i.e. if s = "/")
*/
export function removeTrailingSlash(s: string): string {
if (s.endsWith("/")) {
return s.slice(0, -1);
}
return s;
}
/**
* Checks whether a string is a valid filename. Only used for the filename itself,
* not the entire filepath
@ -39,12 +62,18 @@ export function isValidDirectoryPath(path: string): boolean {
if (t_path.length === 0) { return false; }
if (t_path.length === 1) {
return isValidDirectoryName(t_path);
return t_path === "/";
}
// Leading/Trailing slashes dont matter for this
if (t_path.startsWith("/")) { t_path = t_path.slice(1); }
if (t_path.endsWith("/")) { t_path = t_path.slice(0, -1); }
// A full path must have a leading slash, but we'll ignore it for the checks
if (t_path.startsWith("/")) {
t_path = t_path.slice(1);
} else {
return false;
}
// Trailing slash does not matter
t_path = removeTrailingSlash(t_path);
// Check that every section of the path is a valid directory name
const dirs = t_path.split("/");
@ -69,9 +98,8 @@ export function isValidFilePath(path: string): boolean {
// Impossible for filename to have less than length of 3
if (t_path.length < 3) { return false; }
// Filename can't end with trailing slash. Leading slash can be ignored
if (t_path.endsWith("")) { return false; }
if (t_path.startsWith("/")) { t_path = t_path.slice(1); }
// Full filepath can't end with trailing slash because it must be a file
if (t_path.endsWith("/")) { return false; }
// Everything after the last forward slash is the filename. Everything before
// it is the file path
@ -81,23 +109,55 @@ export function isValidFilePath(path: string): boolean {
}
const fn = t_path.slice(fnSeparator + 1);
const dirPath = t_path.slice(0, fnSeparator);
const dirPath = t_path.slice(0, fnSeparator + 1);
return (isValidDirectoryPath(dirPath) && isValidFilename(fn));
return isValidDirectoryPath(dirPath) && isValidFilename(fn);
}
/**
* Returns a formatter string for the first parent directory in a filepath. For example:
* /home/var/test/ -> home/
* If there is no first parent directory, then it returns "/" for root
*/
export function getFirstParentDirectory(path: string): string {
let t_path = path;
t_path = removeLeadingSlash(t_path);
t_path = removeTrailingSlash(t_path);
let dirs = t_path.split("/");
if (dirs.length === 0) { return "/"; }
return dirs[0] + "/";
}
/**
* Checks if a file path refers to a file in the root directory.
*/
export function isInRootDirectory(path: string): boolean {
if (!isValidFilePath(path)) { return false; }
if (path == null || path.length === 0) { return false; }
return (path.lastIndexOf("/") <= 0);
}
/**
* Evaluates a directory path, including the processing of linux dots.
* Returns the full, proper path, or null if an invalid path is passed in
*/
export function evaluateDirectoryPath(path: string): string | null {
if (!isValidDirectoryPath(path)) { return null; }
export function evaluateDirectoryPath(path: string, currPath?: string): string | null {
let t_path = path;
// If the path begins with a slash, then its an absolute path. Otherwise its relative
// For relative paths, we need to prepend the current directory
if (!t_path.startsWith("/") && currPath != null) {
t_path = currPath + (currPath.endsWith("/") ? "" : "/") + t_path;
}
if (!isValidDirectoryPath(t_path)) { return null; }
// Trim leading/trailing slashes
if (t_path.startsWith("/")) { t_path = t_path.slice(1); }
if (t_path.endsWith("/")) { t_path = t_path.slice(0, -1); }
t_path = removeLeadingSlash(t_path);
t_path = removeTrailingSlash(t_path);
const dirs = t_path.split("/");
const reconstructedPath: string[] = [];
@ -117,5 +177,44 @@ export function evaluateDirectoryPath(path: string): string | null {
}
}
return reconstructedPath.join("/");
return "/" + reconstructedPath.join("/");
}
/**
* Evaluates a file path, including the processing of linux dots.
* Returns the full, proper path, or null if an invalid path is passed in
*/
export function evaluateFilePath(path: string, currPath?: string): string | null {
let t_path = path;
// If the path begins with a slash, then its an absolute path. Otherwise its relative
// For relative paths, we need to prepend the current directory
if (!t_path.startsWith("/") && currPath != null) {
t_path = currPath + (currPath.endsWith("/") ? "" : "/") + t_path;
}
if (!isValidFilePath(t_path)) { return null; }
// Trim leading/trailing slashes
t_path = removeLeadingSlash(t_path);
const dirs = t_path.split("/");
const reconstructedPath: string[] = [];
for (const dir of dirs) {
if (dir === ".") {
// Current directory, do nothing
continue;
} else if (dir === "..") {
// Parent directory
const res = reconstructedPath.pop();
if (res == null) {
return null; // Array was empty, invalid path
}
} else {
reconstructedPath.push(dir);
}
}
return "/" + reconstructedPath.join("/");
}

View File

@ -1,10 +1,13 @@
/* tslint:disable:max-line-length completed-docs variable-name*/
import { IMap } from "../types";
export const TerminalHelpText: string =
"Type 'help name' to learn more about the command 'name'<br><br>" +
'alias [-g] [name="value"] Create or display Terminal aliases<br>' +
"analyze Get information about the current machine <br>" +
"buy [-l/program] Purchase a program through the Dark Web<br>" +
"cat [file] Display a .msg, .lit, or .txt file<br>" +
"cd [dir] Change to a new directory<br>" +
"check [script] [args...] Print a script's logs to Terminal<br>" +
"clear Clear all text on the terminal <br>" +
"cls See 'clear' command <br>" +
@ -19,9 +22,10 @@ export const TerminalHelpText: string =
"ifconfig Displays the IP address of the machine<br>" +
"kill [script] [args...] Stops the specified script on the current server <br>" +
"killall Stops all running scripts on the current machine<br>" +
"ls [| grep pattern] Displays all files on the machine<br>" +
"ls [dir] [| grep pattern] Displays all files on the machine<br>" +
"lscpu Displays the number of CPU cores on the machine<br>" +
"mem [script] [-t] [n] Displays the amount of RAM required to run the script<br>" +
"mv [src] [dest] Move/rename a text or script file<br>" +
"nano [file] Text editor - Open up and edit a script or text file<br>" +
"ps Display all scripts that are currently running<br>" +
"rm [file] Delete a file from the server<br>" +
@ -36,9 +40,6 @@ export const TerminalHelpText: string =
'unalias [alias name] Deletes the specified alias<br>' +
"wget [url] [target file] Retrieves code/text from a web server<br>";
interface IMap<T> {
[key: string]: T;
}
export const HelpTexts: IMap<string> = {
alias: 'alias [-g] [name="value"] <br>' +
"Create or display aliases. An alias enables a replacement of a word with another string. " +
@ -74,6 +75,12 @@ export const HelpTexts: IMap<string> = {
"cat j1.msg<br>" +
"cat foo.lit<br>" +
"cat servers.txt",
cd: "cd [dir]<br>" +
"Change to the specified directory. Note that this works even for directories that don't exist. If you " +
"change to a directory that does not exist, it will not be 'created'. Examples:<br><br>" +
"cd scripts/hacking<br>" +
"cd /logs<br>" +
"cd ../",
check: "check [script name] [args...]<br>" +
"Print the logs of the script specified by the script name and arguments to the Terminal. Each argument must be separated by " +
"a space. Remember that a running script is uniquely " +
@ -135,15 +142,18 @@ export const HelpTexts: IMap<string> = {
"Note that after the 'kill' command is issued for a script, it may take a while for the script to actually stop running. " +
"This will happen if the script is in the middle of a command such as grow() or weaken() that takes time to execute. " +
"The script will not be stopped/killed until after that time has elapsed.",
ls: "ls [| grep pattern]<br>" +
"The ls command, with no arguments, prints all files on the current server to the Terminal screen. " +
"This includes all scripts, programs, and message files. " +
ls: "ls [dir] [| grep pattern]<br>" +
"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. <br><br>" +
"The '| grep pattern' optional parameter can be used to only display files whose filenames match the specified pattern. " +
"For example, if you wanted to only display files with the .script extension, you could use: <br><br>" +
"The 'dir' optional parameter can be used to display files/directories in another directory.<br><br>" +
"The '| grep pattern' optional parameter can be used to only display files whose filenames match the specified pattern.<br><br>" +
"Examples:<br><br>" +
"List all files with the '.script' extension in the current directory:<br>" +
"ls | grep .script<br><br>" +
"Alternatively, if you wanted to display all files with the word purchase in the filename, you could use: <br><br>" +
"ls | grep purchase",
"List all files with the '.js' extension in the root directory:<br>" +
"ls / | grep .js<br><br>" +
"List all files with the word 'purchase' in the filename, in the 'scripts' directory:<br>" +
"ls scripts | grep purchase",
lscpu: "lscpu<br>" +
"Prints the number of CPU Cores the current server has",
mem: "mem [script name] [-t] [num threads]<br>" +
@ -154,6 +164,12 @@ export const HelpTexts: IMap<string> = {
"mem foo.script -t 50<br>" +
"The first example above will print the amount of RAM needed to run 'foo.script' with a single thread. The second example " +
"above will print the amount of RAM needed to run 'foo.script' with 50 threads.",
mv: "mv [src] [dest]<br>" +
"Move the source file to the specified destination. This can also be used to rename files. " +
"Note that this only works for scripts and text files (.txt). This command CANNOT be used to " +
"convert to different file types. Examples: <br><br>" +
"mv hacking-controller.script scripts/hacking-controller.script<br>" +
"mv myScript.js myOldScript.js",
nano: "nano [file name]<br>" +
"Opens up the specified file in the Text Editor. Only scripts (.script) or text files (.txt) can be " +
"edited using the Text Editor. If the file does not already exist, then a new, empty one " +

View File

@ -1,9 +1,53 @@
import { Aliases,
GlobalAliases } from "../Alias";
import { DarkWebItems } from "../DarkWeb/DarkWebItems";
import { Message } from "../Message/Message";
import { IPlayer } from "../PersonObjects/IPlayer"
import { AllServers } from "../Server/AllServers";
import {
getFirstParentDirectory,
isInRootDirectory
} from "./DirectoryHelpers";
import {
Aliases,
GlobalAliases
} from "../Alias";
import { DarkWebItems } from "../DarkWeb/DarkWebItems";
import { Message } from "../Message/Message";
import { IPlayer } from "../PersonObjects/IPlayer"
import { AllServers } from "../Server/AllServers";
// An array of all Terminal commands
const commands = [
"alias",
"analyze",
"cat",
"cd",
"check",
"clear",
"cls",
"connect",
"download",
"expr",
"free",
"hack",
"help",
"home",
"hostname",
"ifconfig",
"kill",
"killall",
"ls",
"lscpu",
"mem",
"mv",
"nano",
"ps",
"rm",
"run",
"scan",
"scan-analyze",
"scp",
"sudov",
"tail",
"theme",
"top"
];
export function determineAllPossibilitiesForTabCompletion(p: IPlayer, input: string, index: number=0): string[] {
let allPos: string[] = [];
@ -12,10 +56,62 @@ export function determineAllPossibilitiesForTabCompletion(p: IPlayer, input: str
const homeComputer = p.getHomeComputer();
input = input.toLowerCase();
//If the command starts with './' and the index == -1, then the user
//has input ./partialexecutablename so autocomplete the script or program
//Put './' in front of each script/executable
if (input.startsWith("./") && index == -1) {
// Helper functions
function addAllCodingContracts() {
for (const cct of currServ.contracts) {
allPos.push(cct.fn);
}
}
function addAllLitFiles() {
for (const file of currServ.messages) {
if (!(file instanceof Message)) {
allPos.push(file);
}
}
}
function addAllMessages() {
for (const file of currServ.messages) {
if (file instanceof Message) {
allPos.push(file.filename);
}
}
}
function addAllPrograms() {
for (const program of currServ.programs) {
allPos.push(program);
}
}
function addAllScripts() {
for (const script of currServ.scripts) {
allPos.push(script.filename);
}
}
function addAllTextFiles() {
for (const txt of currServ.textFiles) {
allPos.push(txt.fn);
}
}
function isCommand(cmd: string) {
let t_cmd = cmd;
if (!t_cmd.endsWith(" ")) {
t_cmd += " ";
}
return input.startsWith(t_cmd);
}
/**
* If the command starts with './' and the index == -1, then the user
* has input ./partialexecutablename so autocomplete the script or program.
* Put './' in front of each script/executable
*/
if (isCommand("./") && index == -1) {
//All programs and scripts
for (var i = 0; i < currServ.scripts.length; ++i) {
allPos.push("./" + currServ.scripts[i].filename);
@ -28,148 +124,96 @@ export function determineAllPossibilitiesForTabCompletion(p: IPlayer, input: str
return allPos;
}
//Autocomplete the command
// Autocomplete the command
if (index == -1) {
return ["alias", "analyze", "cat", "check", "clear", "cls", "connect", "download", "expr",
"free", "hack", "help", "home", "hostname", "ifconfig", "kill", "killall",
"ls", "lscpu", "mem", "nano", "ps", "rm", "run", "scan", "scan-analyze",
"scp", "sudov", "tail", "theme", "top"].concat(Object.keys(Aliases)).concat(Object.keys(GlobalAliases));
return commands.concat(Object.keys(Aliases)).concat(Object.keys(GlobalAliases));
}
if (input.startsWith("buy ")) {
if (isCommand("buy")) {
let options = [];
for (const i in DarkWebItems) {
const item = DarkWebItems[i]
options.push(item.program);
}
return options.concat(Object.keys(GlobalAliases));
}
if (input.startsWith("scp ") && index == 1) {
for (var iphostname in AllServers) {
if (AllServers.hasOwnProperty(iphostname)) {
allPos.push(AllServers[iphostname].ip);
allPos.push(AllServers[iphostname].hostname);
}
if (isCommand("scp") && index === 1) {
for (const iphostname in AllServers) {
allPos.push(AllServers[iphostname].ip);
allPos.push(AllServers[iphostname].hostname);
}
return allPos;
}
if (input.startsWith("scp ") && index == 0) {
//All Scripts and lit files
for (var i = 0; i < currServ.scripts.length; ++i) {
allPos.push(currServ.scripts[i].filename);
}
for (var i = 0; i < currServ.messages.length; ++i) {
if (!(currServ.messages[i] instanceof Message)) {
allPos.push(<string>currServ.messages[i]);
}
}
for (var i = 0; i < currServ.textFiles.length; ++i) {
allPos.push(currServ.textFiles[i].fn);
}
if (isCommand("scp") && index === 0) {
addAllScripts();
addAllLitFiles();
addAllTextFiles();
return allPos;
}
if (input.startsWith("connect ") || input.startsWith("telnet ")) {
//All network connections
if (isCommand("connect")) {
// All network connections
for (var i = 0; i < currServ.serversOnNetwork.length; ++i) {
var serv = AllServers[currServ.serversOnNetwork[i]];
if (serv == null) {continue;}
allPos.push(serv.ip); //IP
allPos.push(serv.hostname); //Hostname
if (serv == null) { continue; }
allPos.push(serv.ip);
allPos.push(serv.hostname);
}
return allPos;
}
if (input.startsWith("kill ") || input.startsWith("tail ") ||
input.startsWith("mem ") || input.startsWith("check ")) {
//All Scripts
for (var i = 0; i < currServ.scripts.length; ++i) {
allPos.push(currServ.scripts[i].filename);
}
if (isCommand("kill") || isCommand("tail") || isCommand("mem") || isCommand("check")) {
addAllScripts();
return allPos;
}
if (input.startsWith("nano ")) {
//Scripts and text files and .fconf
for (var i = 0; i < currServ.scripts.length; ++i) {
allPos.push(currServ.scripts[i].filename);
}
for (var i = 0; i < currServ.textFiles.length; ++i) {
allPos.push(currServ.textFiles[i].fn);
}
if (isCommand("nano")) {
addAllScripts();
addAllTextFiles();
allPos.push(".fconf");
return allPos;
}
if (input.startsWith("rm ")) {
for (let i = 0; i < currServ.scripts.length; ++i) {
allPos.push(currServ.scripts[i].filename);
}
for (let i = 0; i < currServ.programs.length; ++i) {
allPos.push(currServ.programs[i]);
}
for (let i = 0; i < currServ.messages.length; ++i) {
if (!(currServ.messages[i] instanceof Message)) {
allPos.push(<string>currServ.messages[i]);
}
}
for (let i = 0; i < currServ.textFiles.length; ++i) {
allPos.push(currServ.textFiles[i].fn);
}
for (let i = 0; i < currServ.contracts.length; ++i) {
allPos.push(currServ.contracts[i].fn);
}
if (isCommand("rm")) {
addAllScripts();
addAllPrograms();
addAllLitFiles();
addAllTextFiles();
addAllCodingContracts();
return allPos;
}
if (input.startsWith("run ")) {
//All programs, scripts, and contracts
for (let i = 0; i < currServ.scripts.length; ++i) {
allPos.push(currServ.scripts[i].filename);
}
if (isCommand("run")) {
addAllScripts();
addAllPrograms();
addAllCodingContracts();
//Programs are on home computer
for (let i = 0; i < homeComputer.programs.length; ++i) {
allPos.push(homeComputer.programs[i]);
}
for (let i = 0; i < currServ.contracts.length; ++i) {
allPos.push(currServ.contracts[i].fn);
}
return allPos;
}
if (input.startsWith("cat ")) {
for (var i = 0; i < currServ.messages.length; ++i) {
if (currServ.messages[i] instanceof Message) {
const msg: Message = <Message>currServ.messages[i];
allPos.push(msg.filename);
} else {
allPos.push(<string>currServ.messages[i]);
}
}
for (var i = 0; i < currServ.textFiles.length; ++i) {
allPos.push(currServ.textFiles[i].fn);
}
if (isCommand("cat")) {
addAllMessages();
addAllLitFiles();
addAllTextFiles();
return allPos;
}
if (input.startsWith("download ")) {
for (var i = 0; i < currServ.textFiles.length; ++i) {
allPos.push(currServ.textFiles[i].fn);
}
for (var i = 0; i < currServ.scripts.length; ++i) {
allPos.push(currServ.scripts[i].filename);
}
if (isCommand("download") || isCommand("mv")) {
addAllScripts();
addAllTextFiles();
return allPos;
}
if (input.startsWith("ls ")) {
for (var i = 0; i < currServ.textFiles.length; ++i) {
allPos.push(currServ.textFiles[i].fn);
}
for (var i = 0; i < currServ.scripts.length; ++i) {
allPos.push(currServ.scripts[i].filename);
}
}
return allPos;
}

View File

@ -0,0 +1,113 @@
import {
post
} from "../ui/postToTerminal";
import {
containsAllStrings,
longestCommonStart
} from "../../utils/StringHelperFunctions";
/**
* Implements tab completion for the Terminal
*
* @param command {string} Terminal command, excluding the last incomplete argument
* @param arg {string} Last argument that is being completed
* @param allPossibilities {string[]} All values that `arg` can complete to
*/
export function tabCompletion(command: string, arg: string, allPossibilities: string[]): void {
if (!(allPossibilities.constructor === Array)) { return; }
if (!containsAllStrings(allPossibilities)) { return; }
// Remove all options in allPossibilities that do not match the current string
// that we are attempting to autocomplete
if (arg === "") {
for (let i = allPossibilities.length-1; i >= 0; --i) {
if (!allPossibilities[i].toLowerCase().startsWith(command.toLowerCase())) {
allPossibilities.splice(i, 1);
}
}
} else {
for (let i = allPossibilities.length-1; i >= 0; --i) {
if (!allPossibilities[i].toLowerCase().startsWith(arg.toLowerCase())) {
allPossibilities.splice(i, 1);
}
}
}
const textBoxElem = document.getElementById("terminal-input-text-box");
if (textBoxElem == null) {
console.warn(`Couldn't find terminal input DOM element (id=terminal-input-text-box) when trying to autocomplete`);
return;
}
const textBox = <HTMLInputElement>textBoxElem;
const oldValue = textBox.value;
const semiColonIndex = oldValue.lastIndexOf(";");
let val = "";
if (allPossibilities.length === 0) {
return;
} else if (allPossibilities.length === 1) {
if (arg === "") {
//Autocomplete command
val = allPossibilities[0] + " ";
} else {
val = command + " " + allPossibilities[0];
}
if (semiColonIndex === -1) {
// No semicolon, so replace the whole command
textBox.value = val;
} else {
// Replace only after the last semicolon
textBox.value = textBox.value.slice(0, semiColonIndex + 1) + " " + val;
}
textBox.focus();
} else {
const longestStartSubstr = longestCommonStart(allPossibilities);
/**
* If the longest common starting substring of remaining possibilities is the same
* as whatevers already in terminal, just list all possible options. Otherwise,
* change the input in the terminal to the longest common starting substr
*/
let allOptionsStr = "";
for (let i = 0; i < allPossibilities.length; ++i) {
allOptionsStr += allPossibilities[i];
allOptionsStr += " ";
}
if (arg === "") {
if (longestStartSubstr === command) {
post("> " + command);
post(allOptionsStr);
} else {
if (semiColonIndex === -1) {
// No semicolon, so replace the whole command
textBox.value = longestStartSubstr;
} else {
// Replace only after the last semicolon
textBox.value = `${textBox.value.slice(0, semiColonIndex + 1)} ${longestStartSubstr}`;
}
textBox.focus();
}
} else {
if (longestStartSubstr === arg) {
// List all possible options
post("> " + command + " " + arg);
post(allOptionsStr);
} else {
if (semiColonIndex == -1) {
// No semicolon, so replace the whole command
textBox.value = `${command} ${longestStartSubstr}`;
} else {
// Replace only after the last semicolon
textBox.value = `${textBox.value.slice(0, semiColonIndex + 1)} ${command} ${longestStartSubstr}`;
}
textBox.focus();
}
}
}
}

View File

@ -126,6 +126,7 @@ module.exports = (env, argv) => {
}
},
devServer: {
port: 8000,
publicPath: `/`,
},
resolve: {