mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2025-03-11 04:42:34 +01:00
Finished rudimentary filesystem implementation for Terminal
This commit is contained in:
@ -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
|
||||
^^^^
|
||||
|
||||
|
@ -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)."
|
||||
|
@ -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
|
||||
|
@ -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>`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
*/
|
||||
|
1061
src/Terminal.js
1061
src/Terminal.js
File diff suppressed because it is too large
Load Diff
@ -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("/");
|
||||
}
|
||||
|
@ -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 " +
|
@ -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;
|
||||
}
|
||||
|
113
src/Terminal/tabCompletion.ts
Normal file
113
src/Terminal/tabCompletion.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -126,6 +126,7 @@ module.exports = (env, argv) => {
|
||||
}
|
||||
},
|
||||
devServer: {
|
||||
port: 8000,
|
||||
publicPath: `/`,
|
||||
},
|
||||
resolve: {
|
||||
|
Reference in New Issue
Block a user