From e502ae0cf477f1fcffa7f050e24c5dfad1d7872f Mon Sep 17 00:00:00 2001 From: "Felix W. Dekker" Date: Thu, 31 Oct 2019 13:58:28 +0100 Subject: [PATCH] Fix #22 --- src/main/js/main.ts | 2 +- src/main/js/shell.ts | 136 +++++++++++++++++++++++++++++++++++++ src/main/js/terminal.ts | 146 +++++++++------------------------------- 3 files changed, 170 insertions(+), 114 deletions(-) create mode 100644 src/main/js/shell.ts diff --git a/src/main/js/main.ts b/src/main/js/main.ts index dd35371..ad72ea4 100644 --- a/src/main/js/main.ts +++ b/src/main/js/main.ts @@ -12,7 +12,7 @@ addOnLoad(() => { q("#terminalCurrentPrefix") ); // @ts-ignore: Force definition - window.relToAbs = (filename: string) => window.terminal.fileSystem.cwd + filename; + window.relToAbs = (filename: string) => window.terminal.shell.fileSystem.cwd + filename; // @ts-ignore: Force definition window.run = (command: string) => window.terminal.processInput(command); diff --git a/src/main/js/shell.ts b/src/main/js/shell.ts new file mode 100644 index 0000000..bac78ec --- /dev/null +++ b/src/main/js/shell.ts @@ -0,0 +1,136 @@ +import {Commands} from "./commands.js"; +import {FileSystem} from "./fs.js"; +import {asciiHeaderHtml} from "./shared.js"; +import {InputHistory, OutputAction} from "./terminal.js"; +import {UserSession} from "./user-session.js"; + + +/** + * A shell that interacts with the user session and file system to execute commands. + */ +export class Shell { + /** + * The history of the user's inputs. + */ + private readonly inputHistory: InputHistory; + /** + * The user session describing the user that interacts with the terminal. + */ + private readonly userSession: UserSession; + /** + * The file system. + */ + private readonly fileSystem: FileSystem; + /** + * The set of commands that can be executed. + */ + private readonly commands: Commands; + + /** + * The name of the user that the user is currently trying to log in as, or `undefined` if the user is not currently + * trying to log in. + */ + private attemptUser: string | undefined; + + + /** + * Constructs a new shell. + * + * @param inputHistory the history of inputs + */ + constructor(inputHistory: InputHistory) { + this.inputHistory = inputHistory; + this.userSession = new UserSession("felix"); + // @ts-ignore + this.fileSystem = new FileSystem(Cookies.get("files"), Cookies.get("cwd")); + this.commands = new Commands(this.userSession, this.fileSystem); + } + + + /** + * Generates the header that is displayed when a user logs in. + * + * @return the header that is displayed when a user logs in + */ + generateHeader(): string { + return "" + + `${asciiHeaderHtml} + + Student MSc Computer Science + @ TU Delft, the Netherlands + ${(new Date()).toISOString()} + + Type "help" for help. + + `.trimLines(); + } + + /** + * Generates the prefix based on the current state of the terminal. + * + * @return the prefix based on the current state of the terminal + */ + generatePrefix(): string { + if (!this.userSession.isLoggedIn) { + if (this.attemptUser === undefined) + return "login as: "; + else + return `Password for ${this.attemptUser}@fwdekker.com: `; + } else { + if (this.userSession.currentUser === undefined) + throw "User is logged in as undefined."; + + return `${this.userSession.currentUser.name}@fwdekker.com ${this.fileSystem.cwd}> `; + } + } + + /** + * Processes a user's input. + * + * @param input the input to process + */ + run(input: string): OutputAction[] { + if (!this.userSession.isLoggedIn) { + if (this.attemptUser === undefined) { + this.attemptUser = input.trim(); + + this.saveState(); + return [["hide-input", true]]; + } else { + const isLoggedIn = this.userSession.tryLogIn(this.attemptUser, input); + this.attemptUser = undefined; + + this.saveState(); + return [ + ["hide-input", false], + ["append", isLoggedIn ? this.generateHeader() : "Access denied\n"] + ]; + } + } + + const output = this.commands.execute(input.trim()); + this.inputHistory.addEntry(input.trim()); + + if (!this.userSession.isLoggedIn) { + this.inputHistory.clear(); + this.fileSystem.cwd = "/"; + } + + this.saveState(); + return [output]; + } + + + /** + * Saves the shell's state in cookies. + */ + private saveState() { + // @ts-ignore + Cookies.set("files", this.fileSystem.serializedRoot, { + "expires": new Date(new Date().setFullYear(new Date().getFullYear() + 25)), + "path": "/" + }); + // @ts-ignore + Cookies.set("cwd", this.fileSystem.cwd, {"path": "/"}); + } +} diff --git a/src/main/js/terminal.ts b/src/main/js/terminal.ts index c2d83c4..cc1b7f5 100644 --- a/src/main/js/terminal.ts +++ b/src/main/js/terminal.ts @@ -1,7 +1,5 @@ -import {asciiHeaderHtml, moveCaretToEndOf, parseCssPixels} from "./shared.js"; -import {FileSystem} from "./fs.js"; -import {Commands} from "./commands.js"; -import {UserSession} from "./user-session.js"; +import {moveCaretToEndOf, parseCssPixels} from "./shared.js"; +import {Shell} from "./shell.js"; /** @@ -30,28 +28,14 @@ export class Terminal { */ private readonly prefixDiv: HTMLElement; - /** - * The user session describing the user that interacts with the terminal. - */ - private readonly userSession: UserSession; /** * The history of the user's inputs. */ private readonly inputHistory: InputHistory; /** - * The file system. + * The shell that handles input. */ - private readonly fileSystem: FileSystem; - /** - * The set of commands that can be executed. - */ - private readonly commands: Commands; - - /** - * The name of the user that the user is currently trying to log in as, or `undefined` if the user is not currently - * trying to log in. - */ - private attemptUser: string | undefined; + private readonly shell: Shell; /** @@ -68,11 +52,8 @@ export class Terminal { this.output = output; this.prefixDiv = prefixDiv; - this.userSession = new UserSession("felix"); this.inputHistory = new InputHistory(); - // @ts-ignore - this.fileSystem = new FileSystem(Cookies.get("files"), Cookies.get("cwd")); - this.commands = new Commands(this.userSession, this.fileSystem); + this.shell = new Shell(this.inputHistory); this.terminal.addEventListener("click", this.onclick.bind(this)); this.terminal.addEventListener("keypress", this.onkeypress.bind(this)); @@ -97,8 +78,8 @@ export class Terminal { scrollStartPosition = newPosition - (newPosition % this.lineHeight); }); - this.outputText = Terminal.generateHeader(); - this.prefixText = this.generatePrefix(); + this.outputText = this.shell.generateHeader(); + this.prefixText = this.shell.generatePrefix(); this.input.focus(); } @@ -189,42 +170,25 @@ export class Terminal { this.terminal.style.marginBottom = (-lines * this.lineHeight) + "px"; } - /** - * Generates the header that is displayed when a user logs in. + * Returns `true` if and only if the input field does not display the user's input. * - * @return the header that is displayed when a user logs in + * @return `true` if and only if the input field does not display the user's input */ - static generateHeader(): string { - return "" + - `${asciiHeaderHtml} - - Student MSc Computer Science - @ TU Delft, the Netherlands - ${(new Date()).toISOString()} - - Type "help" for help. - - `.trimLines(); + private get isInputHidden(): boolean { + return this.input.classList.contains("terminalCurrentFocusInputHidden"); } /** - * Generates the prefix based on the current state of the terminal. + * Sets whether the input field should display the user's input. * - * @return the prefix based on the current state of the terminal + * @param isInputHidden whether the input field should display the user's input */ - private generatePrefix(): string { - if (!this.userSession.isLoggedIn) { - if (this.attemptUser === undefined) - return "login as: "; - else - return `Password for ${this.attemptUser}@fwdekker.com: `; - } else { - if (this.userSession.currentUser === undefined) - throw "User is logged in as undefined."; - - return `${this.userSession.currentUser.name}@fwdekker.com ${this.fileSystem.cwd}> `; - } + private set isInputHidden(isInputHidden: boolean) { + if (isInputHidden) + this.input.classList.add("terminalCurrentFocusInputHidden"); + else + this.input.classList.remove("terminalCurrentFocusInputHidden"); } @@ -233,41 +197,10 @@ export class Terminal { */ private ignoreInput(): void { this.outputText += `${this.prefixText}${this.inputText}\n`; - this.prefixText = this.generatePrefix(); + this.prefixText = this.shell.generatePrefix(); this.inputText = ""; } - /** - * Continues a login attempt where the user has entered the given input. - * - * The input is either a username if `this.attemptUser` is `undefined`, or a password otherwise. - * - * @param input the user's input. - * @throws if the user is already logged in - */ - private continueLogin(input: string): void { - if (this.userSession.isLoggedIn) - throw "`continueLogin` is called while user is already logged in."; - - if (this.attemptUser === undefined) { - this.outputText += `${this.prefixText}${input.trim()}\n`; - - this.attemptUser = input.trim(); - - this.input.classList.add("terminalCurrentFocusInputHidden"); - } else { - this.outputText += `${this.prefixText}\n`; - - if (this.userSession.tryLogIn(this.attemptUser, input)) - this.outputText += Terminal.generateHeader(); - else - this.outputText += "Access denied\n"; - - this.attemptUser = undefined; - this.input.classList.remove("terminalCurrentFocusInputHidden"); - } - } - /** * Processes a user's input. * @@ -275,43 +208,28 @@ export class Terminal { */ processInput(input: string): void { this.inputText = ""; + this.outputText += `${this.prefixText}${this.isInputHidden ? "" : input.trim()}\n`; - if (!this.userSession.isLoggedIn) { - this.continueLogin(input); - } else { - this.outputText += `${this.prefixText}${input}\n`; - this.inputHistory.addEntry(input); - - const output = this.commands.execute(input.trim()); - switch (output[0]) { + const outputActions = this.shell.run(input); + for (const outputAction of outputActions) { + switch (outputAction[0]) { case "append": - if (output[1] !== "") - this.outputText += output[1] + (output[1].endsWith("\n") ? "" : "\n"); + if (outputAction[1] !== "") + this.outputText += outputAction[1] + ((outputAction[1]).endsWith("\n") ? "" : "\n"); break; case "clear": this.outputText = ""; break; case "nothing": break; - } - - if (!this.userSession.isLoggedIn) { - // If the user is no longer logged in - this.inputHistory.clear(); - this.fileSystem.cwd = "/"; + case "hide-input": + this.isInputHidden = outputAction[1]; + break; } } - this.prefixText = this.generatePrefix(); + this.prefixText = this.shell.generatePrefix(); this.scroll = 0; - - // @ts-ignore - Cookies.set("files", this.fileSystem.serializedRoot, { - "expires": new Date(new Date().setFullYear(new Date().getFullYear() + 25)), - "path": "/" - }); - // @ts-ignore - Cookies.set("cwd", this.fileSystem.cwd, {"path": "/"}); } @@ -373,9 +291,11 @@ export class Terminal { *
  • `["nothing"]` means that no output is to be displayed. Equivalent to `["append", ""]`.
  • *
  • `["clear"]` means that the terminal's history should be cleared.
  • *
  • `["append", string]` means that the given string should be displayed as output.
  • + *
  • `["hide-input", boolean]` means that the input field should not display input if and only if the boolean is + * set to true.
  • * */ -export type OutputAction = ["nothing"] | ["clear"] | ["append", string] +export type OutputAction = ["nothing"] | ["clear"] | ["append", string] | ["hide-input", boolean] /** @@ -389,7 +309,7 @@ export type OutputAction = ["nothing"] | ["clear"] | ["append", string] * Calling `previousEntry` at the highest possible index will return the first entry without incrementing the read index * further. */ -class InputHistory { +export class InputHistory { /** * The list of previous input. */