forked from tools/josh
Fix #22
This commit is contained in:
parent
63bea78ab9
commit
e502ae0cf4
|
@ -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);
|
||||
|
||||
|
|
|
@ -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 <span class="smallScreenOnly">
|
||||
</span>@ <a href="https://www.tudelft.nl/en/">TU Delft</a>, the Netherlands
|
||||
<span class="wideScreenOnly">${(new Date()).toISOString()}
|
||||
</span>
|
||||
Type "<a href="#" onclick="run('help');">help</a>" 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 <span style="color: green;">${this.fileSystem.cwd}</span>> `;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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": "/"});
|
||||
}
|
||||
}
|
|
@ -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 <span class="smallScreenOnly">
|
||||
</span>@ <a href="https://www.tudelft.nl/en/">TU Delft</a>, the Netherlands
|
||||
<span class="wideScreenOnly">${(new Date()).toISOString()}
|
||||
</span>
|
||||
Type "<a href="#" onclick="run('help');">help</a>" 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 <span style="color: green;">${this.fileSystem.cwd}</span>> `;
|
||||
}
|
||||
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] + ((<string>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 = <boolean>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 {
|
|||
* <li>`["nothing"]` means that no output is to be displayed. Equivalent to `["append", ""]`.</li>
|
||||
* <li>`["clear"]` means that the terminal's history should be cleared.</li>
|
||||
* <li>`["append", string]` means that the given string should be displayed as output.</li>
|
||||
* <li>`["hide-input", boolean]` means that the input field should not display input if and only if the boolean is
|
||||
* set to true.</li>
|
||||
* </ul>
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
|
|
Loading…
Reference in New Issue