forked from tools/josh
1
0
Fork 0

Use types and accessibility modifiers

This commit is contained in:
Florine W. Dekker 2019-10-21 02:25:42 +02:00
parent 28010a8cc9
commit fbf0916a5f
Signed by: FWDekker
GPG Key ID: B1B567AF58D6EE0F
4 changed files with 431 additions and 470 deletions

View File

@ -1,117 +1,109 @@
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: "" +
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: {
fun: this.cp,
summary: `copy file`,
usage: `cp SOURCE DESTINATION`,
desc: "" +
),
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: {
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: "" +
),
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: {
fun: this.ls,
summary: `list directory contents`,
usage: `ls [DIRECTORY]`,
desc: "" +
),
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: {
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: "" +
),
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: "" +
),
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:
),
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.
@ -121,71 +113,69 @@ export class Commands {
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: "" +
),
rmdir: new Command(
this.rmdir,
`remove directories`,
`rmdir DIRECTORY...`,
`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: "" +
),
touch: new Command(
this.touch,
`change file timestamps`,
`touch FILE...`,
`Update the access and modification times of each FILE to the current time.
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;

284
js/fs.ts
View File

@ -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 `<a href="#" class="dirLink" onclick="run('cd ${relToAbs(name)}/');run('ls');">${name}/</a>`;
}
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 `<a href="${this.url}" class="fileLink">${name}</a>`;
}
}

View File

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

View File

@ -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(/<br>/, "");
}
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 <span style="color: green;">${this._fs.pwd}</span>&gt; `;
return `${this._currentUser}@fwdekker.com <span style="color: green;">${this.fileSystem.pwd}</span>&gt; `;
}
@ -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(/&nbsp;/, " "));
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;
}