383 lines
12 KiB
TypeScript
383 lines
12 KiB
TypeScript
/**
|
|
* This file is licensed under the GPLv3 license (c) Aleksi 'Allexit' Talarmo
|
|
* See COPYING-GPL and README.md for more information.
|
|
*/
|
|
namespace JavaScriptX {
|
|
export class JSX implements JSTerminal.Program {
|
|
|
|
terminal: JSTerminal.Terminal;
|
|
stdio: Std.IO;
|
|
|
|
filesystem: FileSystem;
|
|
currentFolder: Folder;
|
|
|
|
environmentVariables: { [variable: string]: string } = {
|
|
"system": "~/.system/"
|
|
};
|
|
|
|
create(terminal: JSTerminal.Terminal, stdio: Std.IO, args?: string[]) {
|
|
this.terminal = terminal;
|
|
this.stdio = stdio;
|
|
|
|
this.filesystem = new FileSystem();
|
|
this.currentFolder = this.filesystem.rootFolder;
|
|
|
|
let system = new Folder(".system", true);
|
|
this.filesystem.rootFolder.addFile(system);
|
|
|
|
system.addFile(new BinaryFile("echo", new JSXCommands.Echo()));
|
|
system.addFile(new BinaryFile("ls", new JSXCommands.List(this)));
|
|
system.addFile(new BinaryFile("cd", new JSXCommands.ChangeDirectory(this)));
|
|
system.addFile(new BinaryFile("mkdir", new JSXCommands.MakeDirectory(this)));
|
|
system.addFile(new BinaryFile("woman", new JSXCommands.Woman(this)));
|
|
system.addFile(new BinaryFile("cat", new JSXCommands.Caternate(this)));
|
|
system.addFile(new BinaryFile("touch", new JSXCommands.Touch(this)));
|
|
system.addFile(new BinaryFile("jedi", new JSXCommands.JEdi(this)));
|
|
|
|
this.filesystem.rootFolder.addFile(new TextFile("textfile.txt", "Hello, world!"));
|
|
|
|
stdio.println("Welcome to ");
|
|
stdio.print("JSX", "red");
|
|
stdio.print(" version ");
|
|
stdio.print("0.4.0 beta", "green");
|
|
stdio.print(" (K) All rites reversed");
|
|
stdio.println("\nTo get started: call a ");
|
|
stdio.print("woman", "blue");
|
|
stdio.print(" for help.");
|
|
stdio.refresh();
|
|
|
|
this.takeCommand();
|
|
}
|
|
|
|
/**
|
|
* Inherited from Program-interface. Not to be manually called.
|
|
*/
|
|
enable() {
|
|
this.takeCommand();
|
|
}
|
|
|
|
/**
|
|
* Inherited from Program-interface. Not to be manually called.
|
|
*/
|
|
disable() {
|
|
}
|
|
|
|
/**
|
|
* Inherited from Program-interface. Not to be manually called.
|
|
*/
|
|
onClose() {
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Utility function. Writes a line such as 'Anonymous@localhost: ~/' and then reads line.
|
|
* The result of the readline is used to call commands and such.
|
|
*/
|
|
takeCommand() {
|
|
let name = window.location.hostname || "localhost";
|
|
this.stdio.println(`\nAnonymous@${name}`, "yellow");
|
|
this.stdio.print(":");
|
|
let path = this.currentFolder.getPath();
|
|
this.stdio.print(path, "green");
|
|
this.stdio.readline({
|
|
callback: (result: string) => {
|
|
let split = result.split(" ");
|
|
let command = split[0];
|
|
let args = split.slice(1, split.length);
|
|
|
|
for (let v in this.environmentVariables) {
|
|
if (command == v) {
|
|
command = this.environmentVariables[v];
|
|
break;
|
|
}
|
|
|
|
let file = this.findPath(this.environmentVariables[v]);
|
|
if (file instanceof Folder) {
|
|
for (let i = 0; i < file.files.length; i++) {
|
|
let f = file.files[i];
|
|
|
|
if (command == f.name) {
|
|
command = f.getPath();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for (let i = 0; i < args.length; i++) {
|
|
let regexp = /\${[\w]*}/g;
|
|
let arg = args[i];
|
|
let result: string[];
|
|
while ((result = regexp.exec(arg)) != null) {
|
|
let match = result[0];
|
|
let v = match.substr(2, match.length - 3);
|
|
let toReplace = null;
|
|
|
|
for (let env in this.environmentVariables) {
|
|
if (env == v) {
|
|
toReplace = this.environmentVariables[env];
|
|
break;
|
|
}
|
|
}
|
|
if (toReplace != null) {
|
|
args[i] = arg.replace(new RegExp("\\" + match, "g"), toReplace);
|
|
}
|
|
}
|
|
}
|
|
|
|
let file = this.findPath(command);
|
|
if (file && (
|
|
command.startsWith("./")
|
|
|| command.startsWith("~/")
|
|
|| command.startsWith("../"))) {
|
|
|
|
if (file instanceof BinaryFile) {
|
|
file.run(this.terminal, args);
|
|
return;
|
|
} else {
|
|
this.stdio.println(`${file.name} is not executable`);
|
|
}
|
|
} else {
|
|
if (command.startsWith(".")) {
|
|
this.stdio.println(`File not found`);
|
|
} else {
|
|
this.stdio.println(`Command not found`);
|
|
}
|
|
}
|
|
|
|
this.takeCommand();
|
|
},
|
|
prefix: " > ",
|
|
printAfterDone: true,
|
|
style: "red"
|
|
})
|
|
this.stdio.refresh();
|
|
}
|
|
|
|
/**
|
|
* Returns the file at the specified path.
|
|
* Example usage: findPath("~/somefolder/hello/");
|
|
*/
|
|
findPath(path: string): File {
|
|
let originalPath = path.slice();
|
|
let pathPieces = path.split("/");
|
|
|
|
let relativeFolder = this.currentFolder;
|
|
let fileFound = null;
|
|
for (let i = 0; i < pathPieces.length; i++) {
|
|
let p = pathPieces[i];
|
|
|
|
if (p == "") {
|
|
if (relativeFolder == null) {
|
|
fileFound = null;
|
|
break;
|
|
}
|
|
if (i == pathPieces.length - 1 && relativeFolder instanceof Folder) {
|
|
fileFound = relativeFolder;
|
|
break;
|
|
}
|
|
fileFound = null;
|
|
break;
|
|
} else if (p == "..") {
|
|
if (i == 0) {
|
|
if (this.currentFolder.parentFolder == null) {
|
|
fileFound = null;
|
|
break;
|
|
}
|
|
relativeFolder = this.currentFolder.parentFolder;
|
|
}
|
|
else if (relativeFolder == null) {
|
|
fileFound = null;
|
|
break;
|
|
} else if (relativeFolder.parentFolder == null) {
|
|
fileFound = null;
|
|
break;
|
|
} else {
|
|
relativeFolder = relativeFolder.parentFolder;
|
|
}
|
|
if (i == pathPieces.length - 1) {
|
|
fileFound = relativeFolder;
|
|
break;
|
|
}
|
|
} else if (p == ".") {
|
|
if (i == 0) {
|
|
relativeFolder = this.currentFolder;
|
|
} if (i == pathPieces.length - 1) {
|
|
fileFound = relativeFolder;
|
|
break;
|
|
}
|
|
continue;
|
|
} else if (p == "~" && i == 0) {
|
|
relativeFolder = this.filesystem.rootFolder;
|
|
if (i == pathPieces.length - 1) {
|
|
fileFound = relativeFolder;
|
|
break;
|
|
}
|
|
continue;
|
|
} else if (i == pathPieces.length - 1) {
|
|
if (relativeFolder == null) {
|
|
fileFound = null;
|
|
break;
|
|
}
|
|
let idx = relativeFolder.files.map((f) => { return f.name })
|
|
.indexOf(p);
|
|
let found = relativeFolder.files[idx];
|
|
if (idx == -1) {
|
|
break;
|
|
} else {
|
|
fileFound = found;
|
|
}
|
|
} else {
|
|
if (relativeFolder == null) {
|
|
fileFound = null;
|
|
break;
|
|
}
|
|
let idx = relativeFolder.files.map((f) => { return f.name })
|
|
.indexOf(p);
|
|
let found = relativeFolder.files[idx];
|
|
if (idx == -1) {
|
|
break;
|
|
} else if (found instanceof Folder) {
|
|
relativeFolder = found;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return fileFound;
|
|
}
|
|
|
|
}
|
|
|
|
class FileSystem {
|
|
|
|
rootFolder: Folder;
|
|
|
|
constructor() {
|
|
this.rootFolder = new Folder("~");
|
|
}
|
|
}
|
|
|
|
abstract class File {
|
|
|
|
name: string;
|
|
parentFolder: Folder;
|
|
hidden: boolean;
|
|
created: number = new Date().getTime();
|
|
|
|
constructor(name: string, hidden: boolean = false) {
|
|
this.name = name;
|
|
this.hidden = hidden;
|
|
}
|
|
|
|
/**
|
|
* Returns the string path of the file.
|
|
* Example: If the file is in root folder (~) and inside a 'somefolder'-folder, it would return "~/somefolder/<filename>"
|
|
*/
|
|
getPath(): string {
|
|
let path = "";
|
|
let curr: File = this;
|
|
while (curr != null) {
|
|
if (curr instanceof Folder || path != "") {
|
|
path = "/" + path;
|
|
}
|
|
path = curr.name + path;
|
|
curr = curr.parentFolder;
|
|
}
|
|
return path;
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* File that contains other files.
|
|
*/
|
|
export class Folder extends File {
|
|
|
|
files: File[];
|
|
|
|
constructor(name: string, hidden: boolean = false) {
|
|
super(name, hidden);
|
|
this.files = [];
|
|
}
|
|
|
|
/**
|
|
* Add file to this folder.
|
|
*/
|
|
addFile(file: File) {
|
|
this.files.push(file);
|
|
file.parentFolder = this;
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* File that contains JavaScript program which can be run
|
|
*/
|
|
export class BinaryFile extends File {
|
|
|
|
program: JSTerminal.Program;
|
|
|
|
constructor(name: string, program: JSTerminal.Program, hidden: boolean = false) {
|
|
super(name, hidden);
|
|
this.program = program;
|
|
}
|
|
|
|
/**
|
|
* Launch the program specified in this file.
|
|
*/
|
|
run(terminal: JSTerminal.Terminal, args?: string[]) {
|
|
terminal.launchProgram(this.program, args);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* File that contains text
|
|
*/
|
|
export class TextFile extends File {
|
|
|
|
content: string;
|
|
|
|
constructor(name: string, content: string = "", hidden: boolean = false) {
|
|
super(name, hidden);
|
|
this.content = content;
|
|
}
|
|
|
|
readContent() {
|
|
return this.content.slice();
|
|
}
|
|
|
|
setContent(content: string) {
|
|
this.content = content;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Used as an interface, to give programs documentation that can be read with the 'woman' command.
|
|
* Implement along with Program.
|
|
*/
|
|
export interface Manual {
|
|
getManual(): string;
|
|
}
|
|
|
|
/**
|
|
* Formats the given string to a certain length and alignment.
|
|
* Example: formatToLength("hello", 10, "right") -> ".....hello" (where dots represent spaces)
|
|
*/
|
|
export function formatToLength(text: string, length: number, alignment: "left" | "right" | "center" = "left") {
|
|
if (alignment == "left") {
|
|
text += " ".repeat(length);
|
|
return text.substr(0, text.length - (text.length - length));
|
|
} else if (alignment == "right") {
|
|
text = " ".repeat(length) + text;
|
|
return text.substr(-length);
|
|
} else if (alignment == "center") {
|
|
let left = Math.max(0, Math.floor(length / 2 - text.length / 2));
|
|
let right = Math.max(0, Math.ceil(length / 2 - text.length / 2));
|
|
return " ".repeat(left) + text + " ".repeat(right);
|
|
}
|
|
return null;
|
|
}
|
|
}
|