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

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