diff --git a/js/commands.ts b/js/commands.ts
index 7916234..1e75594 100644
--- a/js/commands.ts
+++ b/js/commands.ts
@@ -1,191 +1,181 @@
import "./extensions.js"
import {FileSystem, UrlFile} from "./fs.js"
-import {terminal} from "./terminal.js";
+import {Terminal, terminal} from "./terminal.js";
export class Commands {
- private _terminal: any;
- private _fs: any;
- private _list: any;
+ private readonly terminal: Terminal;
+ private readonly fileSystem: FileSystem;
+ private readonly commands: object;
- constructor(terminal, fileSystem) {
- this._terminal = terminal;
- this._fs = fileSystem;
-
- this._list = {
- clear: {
- fun: this.clear,
- summary: `clear terminal output`,
- usage: `clear`,
- desc: `Clears all previous terminal output.`
- },
- cd: {
- fun: this.cd,
- summary: `change directory`,
- usage: `cd [DIRECTORY]`,
- desc: "" +
- `Changes the current working directory to [DIRECTORY].
- If [DIRECTORY] is empty, nothing happens.`.trimLines()
- },
- cp: {
- fun: this.cp,
- summary: `copy file`,
- usage: `cp SOURCE DESTINATION`,
- desc: "" +
- `Copies SOURCE to DESTINATION.
- SOURCE must be a file.
- If DESTINATION exists and is a directory, SOURCE is copied into the directory.`.trimLines()
- },
- echo: {
- fun: Commands.echo,
- summary: `display text`,
- usage: `echo [TEXT]`,
- desc: `Displays [TEXT].`.trimLines()
- },
- exit: {
- fun: this.exit,
- summary: `close session`,
- usage: `exit`,
- desc: `Closes the terminal session.`
- },
- help: {
- fun: this.help,
- summary: `display documentation`,
- usage: `help [COMMAND]`,
- desc: "" +
- `Displays help documentation for [COMMAND].
- If [COMMAND] is empty, a list of all commands is shown.`.trimLines()
- },
- ls: {
- fun: this.ls,
- summary: `list directory contents`,
- usage: `ls [DIRECTORY]`,
- desc: "" +
- `Displays the files and directories in [DIRECTORY].
- If [DIRECTORY] is empty, the files and directories in the current working directory are shown.`.trimLines()
- },
- man: {
- fun: this.man,
- summary: `display manual documentation pages`,
- usage: `man PAGE`,
- desc: `Displays the manual page with the name PAGE.`
- },
- mkdir: {
- fun: this.mkdir,
- summary: `make directories`,
- usage: `mkdir DIRECTORY...`,
- desc: "" +
- `Creates the directories given by DIRECTORY.
+ constructor(terminal: Terminal, fileSystem: FileSystem) {
+ this.terminal = terminal;
+ this.fileSystem = fileSystem;
+ this.commands = {
+ clear: new Command(
+ this.clear,
+ `clear terminal output`,
+ `clear`,
+ `Clears all previous terminal output.`.trimLines()
+ ),
+ cd: new Command(
+ this.cd,
+ `change directory`,
+ `cd [DIRECTORY]`,
+ `Changes the current working directory to [DIRECTORY].
+ If [DIRECTORY] is empty, nothing happens.`.trimLines()
+ ),
+ cp: new Command(
+ this.cp,
+ `copy file`,
+ `cp SOURCE DESTINATION`,
+ `Copies SOURCE to DESTINATION.
+ SOURCE must be a file.
+ If DESTINATION exists and is a directory, SOURCE is copied into the directory.`.trimLines()
+ ),
+ echo: new Command(
+ this.echo,
+ `display text`,
+ `echo [TEXT]`,
+ `Displays [TEXT].`.trimLines()
+ ),
+ exit: new Command(
+ this.exit,
+ `close session`,
+ `exit`,
+ `Closes the terminal session.`.trimLines()
+ ),
+ help: new Command(
+ this.help,
+ `display documentation`,
+ `help [COMMAND]`,
+ `Displays help documentation for [COMMAND].
+ If [COMMAND] is empty, a list of all commands is shown.`.trimLines()
+ ),
+ ls: new Command(
+ this.ls,
+ `list directory contents`,
+ `ls [DIRECTORY]`,
+ `Displays the files and directories in [DIRECTORY].
+ If [DIRECTORY] is empty, the files and directories in the current working directory are shown.`.trimLines()
+ ),
+ man: new Command(
+ this.man,
+ `display manual documentation pages`,
+ `man PAGE`,
+ `Displays the manual page with the name PAGE.`.trimLines()
+ ),
+ mkdir: new Command(
+ this.mkdir,
+ `make directories`,
+ `mkdir DIRECTORY...`,
+ `Creates the directories given by DIRECTORY.
- If more than one directory is given, the directories are created in the order they are given in`.trimLines()
- },
- mv: {
- fun: this.mv,
- summary: `move file`,
- usage: `mv SOURCE DESTINATION`,
- desc: `Renames SOURCE to DESTINATION.`
- },
- open: {
- fun: this.open,
- summary: `open web page`,
- usage: `open [-b | --blank] FILE`,
- desc: "" +
- `Opens the web page linked to by FILE in this browser window.
+ If more than one directory is given, the directories are created in the order they are given in`.trimLines()
+ ),
+ mv: new Command(
+ this.mv,
+ `move file`,
+ `mv SOURCE DESTINATION`,
+ `Renames SOURCE to DESTINATION.`.trimLines()
+ ),
+ open: new Command(
+ this.open,
+ `open web page`,
+ `open [-b | --blank] FILE`,
+ `Opens the web page linked to by FILE in this browser window.
- If -b or --blank is set, the web page is opened in a new tab.`.trimLines()
- },
- poweroff: {
- fun: this.poweroff,
- summary: `close down the system`,
- usage: `poweroff`,
- desc: `Automated shutdown procedure to nicely notify users when the system is shutting down.`
- },
- pwd: {
- fun: this.pwd,
- summary: `print working directory`,
- usage: `pwd`,
- desc: `Displays the current working directory.`
- },
- rm: {
- fun: this.rm,
- summary: `remove file`,
- usage: `rm [-f | --force] [-r | -R | --recursive] [--no-preserve-root] FILE...`,
- desc:
- `Removes the files given by FILE.
+ If -b or --blank is set, the web page is opened in a new tab.`.trimLines()
+ ),
+ poweroff: new Command(
+ this.poweroff,
+ `close down the system`,
+ `poweroff`,
+ `Automated shutdown procedure to nicely notify users when the system is shutting down.`.trimLines()
+ ),
+ pwd: new Command(
+ this.pwd,
+ `print working directory`,
+ `pwd`,
+ `Displays the current working directory.`.trimLines()
+ ),
+ rm: new Command(
+ this.rm,
+ `remove file`,
+ `rm [-f | --force] [-r | -R | --recursive] [--no-preserve-root] FILE...`,
+ `Removes the files given by FILE.
+
+ If more than one file is given, the files are removed in the order they are given in.
+
+ If -f or --force is set, no warning is given if a file could not be removed.
+
+ If -r, -R, or --recursive is set, files and directories are removed recursively.
+
+ Unless --no-preserve-root is set, the root directory cannot be removed.`.trimLines()
+ ),
+ rmdir: new Command(
+ this.rmdir,
+ `remove directories`,
+ `rmdir DIRECTORY...`,
+ `Removes the directories given by DIRECTORY.
- If more than one file is given, the files are removed in the order they are given in.
+ If more than one directory is given, the directories are removed in the order they are given in.`.trimLines()
+ ),
+ touch: new Command(
+ this.touch,
+ `change file timestamps`,
+ `touch FILE...`,
+ `Update the access and modification times of each FILE to the current time.
- If -f or --force is set, no warning is given if a file could not be removed.
-
- If -r, -R, or --recursive is set, files and directories are removed recursively.
-
- Unless --no-preserve-root is set, the root directory cannot be removed.`.trimLines()
- },
- rmdir: {
- fun: this.rmdir,
- summary: `remove directories`,
- usage: `rmdir DIRECTORY...`,
- desc: "" +
- `Removes the directories given by DIRECTORY.
-
- If more than one directory is given, the directories are removed in the order they are given in.`.trimLines()
- },
- touch: {
- fun: this.touch,
- summary: `change file timestamps`,
- usage: `touch FILE...`,
- desc: "" +
- `Update the access and modification times of each FILE to the current time.
-
- If a file does not exist, it is created.`.trimLines()
- }
+ If a file does not exist, it is created.`.trimLines()
+ )
};
}
- parse(input) {
+ parse(input: string): string {
const args = new InputArgs(input);
- if (Object.keys(this._list).indexOf(args.getCommand()) >= 0)
- return this._list[args.getCommand()].fun.bind(this)(args);
- else if (args.getCommand().trim() === "")
+ if (args.command.trim() === "")
return "";
+ else if (this.commands.hasOwnProperty(args.command))
+ return this.commands[args.command].fun.bind(this)(args);
else
- return `Unknown command '${args.getCommand()}'`
+ return `Unknown command '${args.command}'`;
}
- cd(args) {
- return this._fs.cd(args.getArg(0));
+ private cd(input: InputArgs): string {
+ return this.fileSystem.cd(input.getArg(0));
}
- cp(args) {
- return this._fs.cp(args.getArg(0), args.getArg(1));
+ private cp(input: InputArgs): string {
+ return this.fileSystem.cp(input.getArg(0), input.getArg(1));
}
- clear() {
- this._terminal.clear();
+ private clear(): string {
+ this.terminal.clear();
return "";
}
- static echo(args) {
- return args.getArgs()
+ private echo(input): string {
+ return input.args
.join(" ")
.replace("hunter2", "*******");
}
- exit() {
- this._terminal.logOut();
+ private exit(): string {
+ this.terminal.logOut();
return "";
}
- help(args) {
- const command = args.getArg(0, "").toLowerCase();
- const commandNames = Object.keys(this._list);
+ private help(input: InputArgs): string {
+ const command = input.getArg(0, "").toLowerCase();
+ const commandNames = Object.keys(this.commands);
if (commandNames.indexOf(command) >= 0) {
- const info = this._list[command];
+ const info = this.commands[command];
return "" +
`${command} - ${info.summary}
@@ -198,7 +188,7 @@ export class Commands {
} else {
const commandWidth = Math.max.apply(null, commandNames.map(it => it.length)) + 4;
const commandEntries = commandNames.map(
- it => `${it.padEnd(commandWidth, ' ')}${this._list[it].summary}`
+ it => `${it.padEnd(commandWidth, ' ')}${this.commands[it].summary}`
);
return "" +
@@ -211,44 +201,44 @@ export class Commands {
}
}
- ls(args) {
- return this._fs.ls(args.getArg(0));
+ private ls(input: InputArgs): string {
+ return this.fileSystem.ls(input.getArg(0));
}
- man(args) {
- if (args.getArgs().length === 0)
+ private man(args: InputArgs): string {
+ if (args.args.length === 0)
return "What manual page do you want?";
- else if (Object.keys(this._list).indexOf(args.getArg(0)) < 0)
+ else if (Object.keys(this.commands).indexOf(args.getArg(0)) < 0)
return `No manual entry for ${args.getArg(0)}`;
else
return this.help(args);
}
- mkdir(args) {
- return this._fs.mkdirs(args.getArgs());
+ private mkdir(args: InputArgs): string {
+ return this.fileSystem.mkdirs(args.args);
}
- mv(args) {
- return this._fs.mv(args.getArg(0), args.getArg(1));
+ private mv(args: InputArgs): string {
+ return this.fileSystem.mv(args.getArg(0), args.getArg(1));
}
- open(args) {
+ private open(args: InputArgs): string {
const fileName = args.getArg(0);
const target = args.hasAnyOption(["b", "blank"]) ? "_blank" : "_self";
- const file = this._fs._getFile(fileName);
- if (file === undefined)
+ const node = this.fileSystem.getNode(fileName);
+ if (node === undefined)
return `The file '${fileName}' does not exist`;
- if (!FileSystem.isFile(file))
+ if (!(node instanceof File))
return `'${fileName}' is not a file`;
- if (!(file instanceof UrlFile))
+ if (!(node instanceof UrlFile))
return `Could not open '${fileName}'`;
- window.open(file.url, target);
+ window.open(node.url, target);
return "";
}
- poweroff() {
+ private poweroff(): string {
const date = new Date();
date.setSeconds(date.getSeconds() + 30);
document.cookie = `poweroff=true; expires=${date.toUTCString()}; path=/`;
@@ -257,7 +247,7 @@ export class Commands {
return "" +
`Shutdown NOW!
- *** FINAL System shutdown message from ${terminal._user}@fwdekker.com ***
+ *** FINAL System shutdown message from ${terminal.currentUser}@fwdekker.com ***
System going down IMMEDIATELY
@@ -265,39 +255,55 @@ export class Commands {
System shutdown time has arrived`.trimLines();
}
- pwd() {
- return this._fs.pwd;
+ private pwd(): string {
+ return this.fileSystem.pwd;
}
- rm(args) {
- return this._fs.rms(
- args.getArgs(),
+ private rm(args: InputArgs): string {
+ return this.fileSystem.rms(
+ args.args,
args.hasAnyOption(["f", "force"]),
args.hasAnyOption(["r", "R", "recursive"]),
args.hasOption("no-preserve-root")
);
}
- rmdir(args) {
- return this._fs.rmdirs(args.getArgs());
+ private rmdir(args: InputArgs): string {
+ return this.fileSystem.rmdirs(args.args);
}
- touch(args) {
- return this._fs.createFiles(args.getArgs());
+ private touch(args: InputArgs): string {
+ return this.fileSystem.createFiles(args.args);
+ }
+}
+
+
+class Command {
+ readonly fun: (args: InputArgs) => string;
+ readonly summary: string;
+ readonly usage: string;
+ readonly desc: string;
+
+
+ constructor(fun: (args: InputArgs) => string, summary: string, usage: string, desc: string) {
+ this.fun = fun;
+ this.summary = summary;
+ this.usage = usage;
+ this.desc = desc;
}
}
class InputArgs {
- private _command: any;
- private _options: any;
- private _args: any;
+ readonly command: string;
+ private readonly _options: object;
+ private readonly _args: string[];
- constructor(input) {
+ constructor(input: string) {
const inputParts = (input.match(/("[^"]+"|[^"\s]+)/g) || [])
.map(it => it.replace(/^"/, "").replace(/"$/, ""));
- this._command = (inputParts[0] || "").toLowerCase();
+ this.command = (inputParts[0] || "").toLowerCase();
this._options = {};
let i;
@@ -320,9 +326,7 @@ class InputArgs {
// -opq
const argNames = argParts[0].substr(1).split("");
- argNames.forEach(argName => {
- this._options[argName] = "";
- });
+ argNames.forEach(argName => this._options[argName] = "");
} else {
// Invalid
throw "Cannot assign value to multiple options!";
@@ -339,35 +343,37 @@ class InputArgs {
}
- getArgs() {
+ get options(): object {
+ return Object.assign({}, this._options);
+ }
+
+ get args(): string[] {
return this._args.slice();
}
- getArg(index, def) {
+
+ getArg(index: number, def: string = undefined): string {
return (def === undefined)
? this._args[index]
- : this._args[index] || def;
+ : (this.hasArg(index) ? this._args[index] : def);
}
- hasArg(index) {
- return (this._args[index] !== undefined);
+ hasArg(index: number): boolean {
+ return index >= 0 && index < this._args.length;
}
- getCommand() {
- return this._command;
- }
- getOption(key, def = undefined) {
+ getOption(key: string, def: string = undefined) {
return (def === undefined)
? this._options[key]
- : this._options[key] || def;
+ : (this.hasOption(key) ? this._options[key] : def);
}
- hasOption(key) {
- return (this.getOption(key) !== undefined);
+ hasOption(key: string): boolean {
+ return this._options.hasOwnProperty(key);
}
- hasAnyOption(keys) {
+ hasAnyOption(keys: string[]): boolean {
for (let i = 0; i < keys.length; i++)
if (this.hasOption(keys[i]))
return true;
diff --git a/js/fs.ts b/js/fs.ts
index 3d68322..5a1c51d 100644
--- a/js/fs.ts
+++ b/js/fs.ts
@@ -1,16 +1,15 @@
import {emptyFunction} from "./shared.js";
-import {relToAbs} from "./terminal.js";
export class FileSystem {
pwd: string;
- private _root: Directory;
+ private root: Directory;
private files: Directory;
constructor() {
this.pwd = "/";
- this._root = new Directory({
+ this.root = new Directory({
personal: new Directory({
steam: new UrlFile("https://steamcommunity.com/id/Waflix"),
nukapedia: new UrlFile("http://fallout.wikia.com/wiki/User:FDekker"),
@@ -32,28 +31,33 @@ export class FileSystem {
}
- _getFile(pathString) {
+ getNode(pathString: string): Node {
const path = new Path(this.pwd, pathString);
- let file = this._root;
+ let node: Node = this.root;
path.parts.forEach(part => {
- if (part === "")
+ if (part === "" || node === undefined || node instanceof File)
return;
- if (file === undefined)
- return;
- if (FileSystem.isFile(file)) {
- file = undefined;
- return;
- }
- file = file.getNode(part);
+ if (node instanceof Directory)
+ node = node.getNode(part);
+ else
+ throw "Node must be file or directory.";
});
- return file;
+ return node;
+ }
+
+ /**
+ * Resets navigation in the file system.
+ */
+ reset() {
+ this.pwd = "/";
+ this.files = this.root;
}
- _executeForEach(inputs, fun) {
+ private executeForEach(inputs: string[], fun: (string) => string): string {
const outputs = [];
inputs.forEach(input => {
@@ -67,70 +71,47 @@ export class FileSystem {
}
- /**
- * Returns true iff {@code node} represents a directory.
- *
- * @param node {Object} a node from the file system
- * @returns {boolean} true iff {@code node} represents a directory
- */
- static isDirectory(node) {
- return node instanceof Directory;
- }
-
- /**
- * Returns true iff {@code node} represents a file.
- *
- * @param node {Object} an object from the file system
- * @returns {boolean} true iff {@code node} represents a file
- */
- static isFile(node) {
- return node instanceof File;
- }
-
-
/**
* Changes the current directory to {@code path}, if it exists.
*
* @param pathString the absolute or relative path to change the current directory to
* @returns {string} an empty string if the change was successful, or an error message explaining what went wrong
*/
- cd(pathString) {
- if (pathString === undefined) {
+ cd(pathString: string): string {
+ if (pathString === undefined)
return "";
- }
const path = new Path(this.pwd, pathString);
- const file = this._getFile(path.path);
- if (file === undefined)
+ const node = this.getNode(path.path);
+ if (node === undefined)
return `The directory '${path.path}' does not exist`;
- if (!FileSystem.isDirectory(file))
- return `'${path.path}' is not a directory`;
+ if (!(node instanceof Directory))
+ return `'${path.path}' is not a directory.`;
this.pwd = path.path;
- this.files = file;
-
+ this.files = node;
return "";
}
/**
* Creates an empty file at {@code path} if it does not exist.
*
- * @param path the path to create a file at if it does not exist
+ * @param pathString the path to create a file at if it does not exist
* @returns {string} an empty string if the removal was successful, or a message explaining what went wrong
*/
- createFile(path) {
- path = new Path(this.pwd, path);
+ private createFile(pathString: string): string {
+ const path = new Path(this.pwd, pathString);
- const headNode = this._getFile(path.head);
+ const headNode = this.getNode(path.head);
if (headNode === undefined)
return `The directory '${path.head}' does not exist`;
- if (!FileSystem.isDirectory(headNode))
+ if (!(headNode instanceof Directory))
return `${path.head} is not a directory`;
const tailNode = headNode.getNode(path.tail);
if (tailNode !== undefined)
- return "";
+ return ""; // File already exists
headNode.addNode(path.tail, new File());
return "";
@@ -142,10 +123,8 @@ export class FileSystem {
* @param paths {string[]} the absolute or relative paths to the files to be created
* @returns {string} the warnings generated during creation of the files
*/
- createFiles(paths) {
- return this._executeForEach(paths, path => {
- return this.createFile(path);
- });
+ createFiles(paths: string[]): string {
+ return this.executeForEach(paths, path => this.createFile(path));
}
/**
@@ -159,13 +138,13 @@ export class FileSystem {
* @param destinationString {string} the absolute or relative path to the destination
* @returns {string} an empty string if the copy was successful, or a message explaining what went wrong
*/
- cp(sourceString, destinationString) {
+ cp(sourceString: string, destinationString: string): string {
const sourcePath = new Path(this.pwd, sourceString);
- const sourceTailNode = this._getFile(sourcePath.path);
+ const sourceTailNode = this.getNode(sourcePath.path);
const destinationPath = new Path(this.pwd, destinationString);
- const destinationHeadNode = this._getFile(destinationPath.head);
- const destinationTailNode = this._getFile(destinationPath.path);
+ const destinationHeadNode = this.getNode(destinationPath.head);
+ const destinationTailNode = this.getNode(destinationPath.path);
if (sourceTailNode === undefined)
return `The file '${sourcePath.path}' does not exist`;
@@ -198,27 +177,27 @@ export class FileSystem {
* @param pathString {string} the absolute or relative path to the directory to return
* @returns {Object} the directory at {@code path}, or the current directory if no path is given
*/
- ls(pathString) {
+ ls(pathString: string): string {
const path = new Path(this.pwd, pathString);
- const dir = this._getFile(path.path);
- if (dir === undefined)
+ const node = this.getNode(path.path);
+ if (node === undefined)
return `The directory '${path.path}' does not exist`;
- if (!FileSystem.isDirectory(dir))
+ if (!(node instanceof Directory))
return `'${path.path}' is not a directory`;
const dirList = [new Directory({}).nameString("."), new Directory({}).nameString("..")];
const fileList = [];
- const nodes = dir.getNodes();
+ const nodes = node.nodes;
Object.keys(nodes)
.sortAlphabetically((x) => x)
.forEach(name => {
const node = nodes[name];
- if (FileSystem.isDirectory(node))
+ if (node instanceof Directory)
dirList.push(node.nameString(name));
- else if (FileSystem.isFile(node))
+ else if (node instanceof File)
fileList.push(node.nameString(name));
else
throw `${name} is neither a file nor a directory!`;
@@ -233,13 +212,13 @@ export class FileSystem {
* @param pathString {string} the absolute or relative path to the directory to create
* @returns {string} an empty string if the removal was successful, or a message explaining what went wrong
*/
- mkdir(pathString) {
+ private mkdir(pathString: string): string {
const path = new Path(pathString, undefined);
- const headNode = this._getFile(path.head);
+ const headNode = this.getNode(path.head);
if (headNode === undefined)
return `The directory '${path.head}' does not exist`;
- if (!FileSystem.isDirectory(headNode))
+ if (!(headNode instanceof Directory))
return `'${path.head}' is not a directory`;
if (headNode.getNode(path.tail))
return `The directory '${path.tail}' already exists`;
@@ -254,8 +233,8 @@ export class FileSystem {
* @param paths {string[]} the absolute or relative paths to the directories to create
* @returns {string} the warnings generated during creation of the directories
*/
- mkdirs(paths) {
- return this._executeForEach(paths, this.mkdir.bind(this));
+ mkdirs(paths: string[]): string {
+ return this.executeForEach(paths, this.mkdir.bind(this));
}
/**
@@ -269,15 +248,17 @@ export class FileSystem {
* @param destinationString {string} the absolute or relative path to the destination
* @returns {string} an empty string if the move was successful, or a message explaining what went wrong
*/
- mv(sourceString, destinationString) {
+ mv(sourceString: string, destinationString: string): string {
const sourcePath = new Path(sourceString, undefined);
- const sourceHeadNode = this._getFile(sourcePath.head);
- const sourceTailNode = this._getFile(sourcePath.path);
+ const sourceHeadNode = this.getNode(sourcePath.head);
+ const sourceTailNode = this.getNode(sourcePath.path);
const destinationPath = new Path(destinationString, undefined);
- const destinationHeadNode = this._getFile(destinationPath.head);
- const destinationTailNode = this._getFile(destinationPath.path);
+ const destinationHeadNode = this.getNode(destinationPath.head);
+ const destinationTailNode = this.getNode(destinationPath.path);
+ if (!(sourceHeadNode instanceof Directory))
+ return `The path '${sourcePath.head}' does not point to a directory`;
if (sourceTailNode === undefined)
return `The file '${sourcePath.path}' does not exist`;
if (destinationHeadNode === undefined)
@@ -297,20 +278,11 @@ export class FileSystem {
return `The file '${targetName}' already exists`;
sourceHeadNode.removeNode(sourceTailNode);
- targetNode.addNode(sourceTailNode, undefined);
- sourceTailNode.name = targetName;
+ targetNode.addNode(targetName, sourceTailNode);
return "";
}
- /**
- * Resets navigation in the file system.
- */
- reset() {
- this.pwd = "/";
- this.files = this._root;
- }
-
/**
* Removes a file from the file system.
*
@@ -320,20 +292,20 @@ export class FileSystem {
* @param noPreserveRoot {boolean} false if the root directory should not be removed
* @returns {string} an empty string if the removal was successful, or a message explaining what went wrong
*/
- rm(pathString, force = false, recursive = false, noPreserveRoot = false) {
+ private rm(pathString: string, force: boolean = false, recursive: boolean = false, noPreserveRoot: boolean = false): string {
const path = new Path(pathString, undefined);
- const parentNode = this._getFile(path.head);
+ const parentNode = this.getNode(path.head);
if (parentNode === undefined)
return force
? ""
: `The directory '${path.head}' does not exist`;
- if (!FileSystem.isDirectory(parentNode))
+ if (!(parentNode instanceof Directory))
return force
? ""
: `'${path.head}' is not a directory`;
- const childNode = this._getFile(path.path);
+ const childNode = this.getNode(path.path);
if (childNode === undefined)
return force
? ""
@@ -342,7 +314,7 @@ export class FileSystem {
if (recursive) {
if (path.path === "/")
if (noPreserveRoot)
- this._root = new Directory();
+ this.root = new Directory();
else
return "'/' cannot be removed";
else
@@ -367,8 +339,8 @@ export class FileSystem {
* @param noPreserveRoot {boolean} false if the root directory should not be removed
* @returns {string} the warnings generated during removal of the directories
*/
- rms(paths, force = false, recursive = false, noPreserveRoot = false) {
- return this._executeForEach(paths, path => {
+ rms(paths: string[], force: boolean = false, recursive: boolean = false, noPreserveRoot: boolean = false): string {
+ return this.executeForEach(paths, path => {
return this.rm(path, force, recursive, noPreserveRoot);
});
}
@@ -379,28 +351,28 @@ export class FileSystem {
* @param pathString {string} the absolute or relative path to the directory to be removed
* @returns {string} an empty string if the removal was successful, or a message explaining what went wrong
*/
- rmdir(pathString) {
+ private rmdir(pathString: string): string {
const path = new Path(pathString, undefined);
if (path.path === "/") {
- if (this._root.getNodeCount() > 0)
+ if (this.root.nodeCount > 0)
return `The directory is not empty.`;
else
return "";
}
- const parentDir = this._getFile(path.head);
+ const parentDir = this.getNode(path.head);
if (parentDir === undefined)
return `The directory '${path.head}' does not exist`;
- if (!FileSystem.isDirectory(parentDir))
+ if (!(parentDir instanceof Directory))
return `'${path.head}' is not a directory`;
const childDir = parentDir.getNode(path.tail);
if (childDir === undefined)
return `The directory '${path.tail}' does not exist`;
- if (!FileSystem.isDirectory(childDir))
+ if (!(childDir instanceof Directory))
return `'${path.tail}' is not a directory`;
- if (childDir.getNodeCount() > 0)
+ if (childDir.nodeCount > 0)
return `The directory is not empty`;
parentDir.removeNode(childDir);
@@ -413,22 +385,20 @@ export class FileSystem {
* @param paths {string[]} the absolute or relative paths to the directories to be removed
* @returns {string} the warnings generated during removal of the directories
*/
- rmdirs(paths) {
- return this._executeForEach(paths, path => {
- return this.rmdir(path);
- });
+ rmdirs(paths: string[]): string {
+ return this.executeForEach(paths, path => this.rmdir(path));
}
}
export class Path {
- private _path: string;
- private _parts: string[];
- private _head: string;
- private _tail: string;
+ private readonly _parts: string[];
+ readonly path: string;
+ readonly head: string;
+ readonly tail: string;
- constructor(currentPath, relativePath) {
+ constructor(currentPath: string, relativePath: string) {
let path;
if (relativePath === undefined)
path = currentPath;
@@ -437,78 +407,62 @@ export class Path {
else
path = `${currentPath}/${relativePath}`;
-
- this._path = `${path}/`
+ this.path = `${path}/`
.replaceAll(/\/\.\//, "/")
.replaceAll(/(\/+)([^./]+)(\/+)(\.\.)(\/+)/, "/")
.replaceAll(/\/{2,}/, "/")
.replace(/^\/?\.?\.\/$/, "/")
.toString();
- this._parts = this._path.split("/");
- this._head = this._parts.slice(0, -2).join("/");
- this._tail = this._parts.slice(0, -1).slice(-1).join("/");
+ this._parts = this.path.split("/");
+ this.head = this.parts.slice(0, -2).join("/");
+ this.tail = this.parts.slice(0, -1).slice(-1).join("/");
}
- get path() {
- return this._path;
- }
-
- get parts() {
+ get parts(): string[] {
return this._parts.slice();
}
-
- get head() {
- return this._head;
- }
-
- get tail() {
- return this._tail;
- }
}
-export class Node {
- copy() {
- throw "Cannot execute abstract method!";
- }
+export abstract class Node {
+ abstract copy(): Node;
- nameString(name) {
- throw "Cannot execute abstract method!";
- }
+ abstract nameString(name: string): string;
- visit(fun, pre, post) {
- throw "Cannot execute abstract method!";
- }
+ abstract visit(fun: (node: Node) => void, pre: (node: Node) => void, post: (node: Node) => void);
}
export class Directory extends Node {
- private _parent: this;
- private _nodes: {};
- name: string;
+ private readonly _nodes: object;
+ // noinspection TypeScriptFieldCanBeMadeReadonly: False positive
+ private _parent: Directory;
- constructor(nodes = {}) {
+ constructor(nodes: object = {}) {
super();
this._parent = this;
this._nodes = nodes;
- Object.keys(this._nodes).forEach(name => {
- this._nodes[name]._parent = this
- });
+ Object.keys(this._nodes).forEach(name => this._nodes[name]._parent = this);
}
- getNodes() {
+ get nodes(): object {
return Object.assign({}, this._nodes);
}
- getNodeCount() {
+ get nodeCount(): number {
return Object.keys(this._nodes).length;
}
- getNode(name) {
+ get parent(): Directory {
+ return this._parent;
+ }
+
+
+ getNode(name: string): Node {
switch (name) {
case ".":
return this;
@@ -519,15 +473,14 @@ export class Directory extends Node {
}
}
- addNode(name, node) {
- if (node instanceof Directory) {
+ addNode(name: string, node: Node) {
+ if (node instanceof Directory)
node._parent = this;
- }
this._nodes[name] = node;
}
- removeNode(nodeOrName) {
+ removeNode(nodeOrName: Node | string) {
if (nodeOrName instanceof Node) {
const name = Object.keys(this._nodes).find(key => this._nodes[key] === nodeOrName);
delete this._nodes[name];
@@ -537,23 +490,24 @@ export class Directory extends Node {
}
- copy() {
- const copy = new Directory(Object.assign({}, this._nodes));
+ copy(): Directory {
+ const copy = new Directory(this.nodes);
copy._parent = undefined;
return copy;
}
- nameString(name) {
+ nameString(name: string): string {
+ // @ts-ignore: Defined in `terminal.ts`
return `${name}/`;
}
- visit(fun, pre: (dir: Directory) => void = emptyFunction, post: (dir: Directory) => void = emptyFunction) {
+ visit(fun: (node: Node) => void,
+ pre: (node: Node) => void = emptyFunction,
+ post: (node: Node) => void = emptyFunction) {
pre(this);
fun(this);
- Object.keys(this._nodes).forEach(name => {
- this._nodes[name].visit(fun, pre, post);
- });
+ Object.keys(this._nodes).forEach(name => this._nodes[name].visit(fun, pre, post));
post(this);
}
@@ -565,15 +519,17 @@ export class File extends Node {
}
- copy() {
+ copy(): File {
return new File();
}
- nameString(name) {
+ nameString(name: string): string {
return name;
}
- visit(fun, pre: (dir: File) => void = emptyFunction, post: (dir: File) => void = emptyFunction) {
+ visit(fun: (node: Node) => void,
+ pre: (node: Node) => void = emptyFunction,
+ post: (node: Node) => void = emptyFunction) {
pre(this);
fun(this);
post(this);
@@ -581,21 +537,21 @@ export class File extends Node {
}
export class UrlFile extends File {
- url: any;
+ readonly url: string;
- constructor(url) {
+ constructor(url: string) {
super();
this.url = url;
}
- copy() {
+ copy(): UrlFile {
return new UrlFile(this.url);
}
- nameString(name) {
+ nameString(name: string): string {
return `${name}`;
}
}
diff --git a/js/shared.ts b/js/shared.ts
index 700a4c9..d62fe55 100644
--- a/js/shared.ts
+++ b/js/shared.ts
@@ -11,11 +11,10 @@ export const emptyFunction = () => {};
export function addOnLoad(fun: () => void) {
- const oldOnLoad = window.onload || (() => {
- });
+ const oldOnLoad = window.onload || emptyFunction;
window.onload = (() => {
- // @ts-ignore TODO Find out how to resolve this
+ // @ts-ignore: Call works without parameters as well
oldOnLoad();
fun();
});
@@ -31,6 +30,6 @@ export function moveCaretToEndOf(element: Node) {
selection.addRange(range);
}
-export function q(query: string) {
+export function q(query: string): HTMLElement {
return document.querySelector(query);
}
diff --git a/js/terminal.ts b/js/terminal.ts
index df0d741..c560cc0 100644
--- a/js/terminal.ts
+++ b/js/terminal.ts
@@ -4,66 +4,72 @@ import {Commands} from "./commands.js";
export class Terminal {
- private _terminal: any;
- private _input: any;
- private _output: any;
- private _prefixDiv: any;
- private _user: string;
- private _loggedIn: boolean;
- private _inputHistory: InputHistory;
- private _fs:FileSystem;
- private _commands: Commands;
+ private readonly terminal: HTMLElement;
+ private readonly input: HTMLElement;
+ private readonly output: HTMLElement;
+ private readonly prefixDiv: HTMLElement;
+
+ private readonly inputHistory: InputHistory;
+ private readonly fileSystem: FileSystem;
+ private readonly commands: Commands;
+
+ private _currentUser: string;
+ private isLoggedIn: boolean;
- constructor(terminal, input, output, prefixDiv) {
- this._terminal = terminal;
- this._input = input;
- this._output = output;
- this._prefixDiv = prefixDiv;
+ constructor(terminal: HTMLElement, input: HTMLElement, output: HTMLElement, prefixDiv: HTMLElement) {
+ this.terminal = terminal;
+ this.input = input;
+ this.output = output;
+ this.prefixDiv = prefixDiv;
- this._user = "felix";
- this._loggedIn = true;
+ this._currentUser = "felix";
+ this.isLoggedIn = true;
- this._inputHistory = new InputHistory();
- this._fs = new FileSystem();
- this._commands = new Commands(this, this._fs);
+ this.inputHistory = new InputHistory();
+ this.fileSystem = new FileSystem();
+ this.commands = new Commands(this, this.fileSystem);
- this._terminal.addEventListener("click", this._onclick.bind(this));
- this._terminal.addEventListener("keypress", this._onkeypress.bind(this));
- this._terminal.addEventListener("keydown", this._onkeydown.bind(this));
+ this.terminal.addEventListener("click", this.onclick.bind(this));
+ this.terminal.addEventListener("keypress", this.onkeypress.bind(this));
+ this.terminal.addEventListener("keydown", this.onkeydown.bind(this));
this.reset();
- this._input.focus();
+ this.input.focus();
}
- get inputText() {
- return this._input.innerHTML
+ get inputText(): string {
+ return this.input.innerHTML
.replaceAll(/
/, "");
}
- set inputText(inputText) {
- this._input.innerHTML = inputText;
+ set inputText(inputText: string) {
+ this.input.innerHTML = inputText;
}
- get outputText() {
- return this._output.innerHTML;
+ get outputText(): string {
+ return this.output.innerHTML;
}
- set outputText(outputText) {
- this._output.innerHTML = outputText;
+ set outputText(outputText: string) {
+ this.output.innerHTML = outputText;
}
- get prefixText() {
- return this._prefixDiv.innerHTML;
+ get prefixText(): string {
+ return this.prefixDiv.innerHTML;
}
- set prefixText(prefixText) {
- this._prefixDiv.innerHTML = prefixText;
+ set prefixText(prefixText: string) {
+ this.prefixDiv.innerHTML = prefixText;
+ }
+
+ get currentUser(): string {
+ return this._currentUser;
}
- static generateHeader() {
+ static generateHeader(): string {
return "" +
`${asciiHeaderHtml}
@@ -76,16 +82,15 @@ export class Terminal {
`.trimLines();
}
- generatePrefix() {
- if (!this._loggedIn) {
- if (this._user === undefined) {
+ generatePrefix(): string {
+ if (!this.isLoggedIn) {
+ if (this._currentUser === undefined)
return "login as: ";
- } else {
- return `Password for ${this._user}@fwdekker.com: `;
- }
+ else
+ return `Password for ${this._currentUser}@fwdekker.com: `;
}
- return `${this._user}@fwdekker.com ${this._fs.pwd}> `;
+ return `${this._currentUser}@fwdekker.com ${this.fileSystem.pwd}> `;
}
@@ -94,39 +99,39 @@ export class Terminal {
}
reset() {
- this._fs.reset();
+ this.fileSystem.reset();
this.outputText = Terminal.generateHeader();
this.prefixText = this.generatePrefix();
}
- continueLogin(input) {
- if (this._user === undefined) {
+ private continueLogin(input: string) {
+ if (this._currentUser === undefined) {
this.outputText += `${this.prefixText}${input}\n`;
- this._user = input.trim();
- this._input.classList.add("terminalCurrentFocusInputHidden");
+ this._currentUser = input.trim();
+ this.input.classList.add("terminalCurrentFocusInputHidden");
} else {
this.outputText += `${this.prefixText}\n`;
- if ((this._user === "felix" && input === "hotel123")
- || (this._user === "root" && input === "password")) {
- this._loggedIn = true;
+ if ((this._currentUser === "felix" && input === "password")
+ || (this._currentUser === "root" && input === "root")) {
+ this.isLoggedIn = true;
this.outputText += Terminal.generateHeader();
} else {
- this._user = undefined;
+ this._currentUser = undefined;
this.outputText += "Access denied\n";
}
- this._input.classList.remove("terminalCurrentFocusInputHidden");
+ this.input.classList.remove("terminalCurrentFocusInputHidden");
}
}
logOut() {
- this._user = undefined;
- this._loggedIn = false;
- this._inputHistory.clear();
+ this._currentUser = undefined;
+ this.isLoggedIn = false;
+ this.inputHistory.clear();
}
ignoreInput() {
@@ -135,52 +140,51 @@ export class Terminal {
this.inputText = "";
}
- processInput(input) {
+ processInput(input: string) {
this.inputText = "";
- if (!this._loggedIn) {
+ if (!this.isLoggedIn) {
this.continueLogin(input);
} else {
this.outputText += `${this.prefixText}${input}\n`;
- this._inputHistory.addEntry(input);
+ this.inputHistory.addEntry(input);
- const output = this._commands.parse(input.trim());
- if (output !== "") {
+ const output = this.commands.parse(input.trim());
+ if (output !== "")
this.outputText += output + `\n`;
- }
}
this.prefixText = this.generatePrefix();
}
- _onclick() {
- this._input.focus();
+ private onclick() {
+ this.input.focus();
}
- _onkeypress(e) {
- switch (e.key.toLowerCase()) {
+ private onkeypress(event) {
+ switch (event.key.toLowerCase()) {
case "enter":
this.processInput(this.inputText.replaceAll(/ /, " "));
- e.preventDefault();
+ event.preventDefault();
break;
}
}
- _onkeydown(e) {
- switch (e.key.toLowerCase()) {
+ private onkeydown(event) {
+ switch (event.key.toLowerCase()) {
case "arrowup":
- this.inputText = this._inputHistory.previousEntry();
- window.setTimeout(() => moveCaretToEndOf(this._input), 0);
+ this.inputText = this.inputHistory.previousEntry();
+ window.setTimeout(() => moveCaretToEndOf(this.input), 0);
break;
case "arrowdown":
- this.inputText = this._inputHistory.nextEntry();
- window.setTimeout(() => moveCaretToEndOf(this._input), 0);
+ this.inputText = this.inputHistory.nextEntry();
+ window.setTimeout(() => moveCaretToEndOf(this.input), 0);
break;
case "c":
- if (e.ctrlKey) {
+ if (event.ctrlKey) {
this.ignoreInput();
- e.preventDefault();
+ event.preventDefault();
}
break;
}
@@ -188,54 +192,53 @@ export class Terminal {
}
class InputHistory {
- private _history: string[];
- private _index: number;
+ private history: string[];
+ private index: number;
constructor() {
- this._history = [];
- this._index = -1;
+ this.clear();
}
addEntry(entry: string) {
if (entry.trim() !== "")
- this._history.unshift(entry);
+ this.history.unshift(entry);
- this._index = -1;
+ this.index = -1;
}
clear() {
- this._history = [];
- this._index = -1;
+ this.history = [];
+ this.index = -1;
}
- getEntry(index) {
+ getEntry(index: number): string {
if (index >= 0)
- return this._history[index];
+ return this.history[index];
else
return "";
}
- nextEntry() {
- this._index--;
- if (this._index < -1)
- this._index = -1;
+ nextEntry(): string {
+ this.index--;
+ if (this.index < -1)
+ this.index = -1;
- return this.getEntry(this._index);
+ return this.getEntry(this.index);
}
- previousEntry() {
- this._index++;
- if (this._index >= this._history.length)
- this._index = this._history.length - 1;
+ previousEntry(): string {
+ this.index++;
+ if (this.index >= this.history.length)
+ this.index = this.history.length - 1;
- return this.getEntry(this._index);
+ return this.getEntry(this.index);
}
}
-export let terminal;
+export let terminal: Terminal;
addOnLoad(() => {
terminal = new Terminal(
@@ -245,13 +248,10 @@ addOnLoad(() => {
q("#terminalCurrentPrefix")
);
+ // @ts-ignore: Force definition
+ window.relToAbs = (filename: string) => terminal.fileSystem.pwd + filename;
+ // @ts-ignore: Force definition
+ window.run = (command: string) => terminal.processInput(command);
+
terminal.processInput("ls");
});
-
-export function run(command: string) {
- terminal.processInput(command);
-}
-
-export function relToAbs(filename) {
- return terminal._fs.pwd + filename;
-}