forked from tools/josh
1
0
Fork 0
This commit is contained in:
Florine W. Dekker 2019-10-31 13:58:28 +01:00
parent 63bea78ab9
commit e502ae0cf4
Signed by: FWDekker
GPG Key ID: B1B567AF58D6EE0F
3 changed files with 170 additions and 114 deletions

View File

@ -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);

136
src/main/js/shell.ts Normal file
View File

@ -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>&gt; `;
}
}
/**
* 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": "/"});
}
}

View File

@ -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>&gt; `;
}
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.
*/