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; -}