/** * 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/" */ 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; } }