parent
90701ab1eb
commit
f8415552f9
Binary file not shown.
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "fwdekker.com",
|
||||
"version": "0.24.4",
|
||||
"version": "0.25.0",
|
||||
"description": "The source code of [my personal website](https://fwdekker.com/).",
|
||||
"author": "Felix W. Dekker",
|
||||
"repository": {
|
||||
|
@ -16,13 +16,15 @@
|
|||
"coverage": "nyc npm run test"
|
||||
},
|
||||
"dependencies": {
|
||||
"js-cookie": "^2.2.1"
|
||||
"js-cookie": "^2.2.1",
|
||||
"semver": "^6.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@istanbuljs/nyc-config-typescript": "^0.1.3",
|
||||
"@types/chai": "^4.2.5",
|
||||
"@types/js-cookie": "^2.2.4",
|
||||
"@types/mocha": "^5.2.7",
|
||||
"@types/semver": "^6.2.0",
|
||||
"chai": "^4.2.0",
|
||||
"grunt": "^1.0.4",
|
||||
"grunt-cli": "^1.3.2",
|
||||
|
@ -33,7 +35,7 @@
|
|||
"mocha": "^6.2.2",
|
||||
"nyc": "^14.1.1",
|
||||
"ts-loader": "^6.2.1",
|
||||
"ts-node": "^8.5.2",
|
||||
"ts-node": "^8.5.4",
|
||||
"typescript": "^3.7.2",
|
||||
"webpack": "^4.41.2",
|
||||
"webpack-cli": "^3.3.10"
|
||||
|
|
|
@ -4,7 +4,7 @@ import {Directory, File, FileSystem, Path} from "./FileSystem"
|
|||
import {InputArgs} from "./InputArgs";
|
||||
import {InputParser} from "./InputParser";
|
||||
import {Persistence} from "./Persistence";
|
||||
import {escapeHtml, IllegalArgumentError, IllegalStateError, isStandalone} from "./Shared";
|
||||
import {escapeHtml, ExpectedGoodbyeError, IllegalArgumentError, IllegalStateError, isStandalone} from "./Shared";
|
||||
import {EscapeCharacters} from "./Terminal";
|
||||
import {UserList} from "./UserList";
|
||||
import {StreamSet} from "./Stream";
|
||||
|
@ -60,7 +60,7 @@ export class Commands {
|
|||
`concatenate and print files`,
|
||||
`cat [<b>-e</b> | <b>--escape-html</b>] <u>file</u> <u>...</u>`,
|
||||
`Reads files sequentially, writing them to the standard output.
|
||||
|
||||
|
||||
If the file contains valid HTML, it will be displayed as such by default. If the <b>--html</b> \\\
|
||||
option is given, special HTML characters are escaped and the raw text contents can be inspected.\\\
|
||||
`.trimMultiLines(),
|
||||
|
@ -270,7 +270,7 @@ export class Commands {
|
|||
if (input.command === "factory-reset") {
|
||||
Persistence.reset();
|
||||
location.reload();
|
||||
throw new Error("Goodbye");
|
||||
throw new ExpectedGoodbyeError("Goodbye");
|
||||
}
|
||||
|
||||
if (input.command === "")
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {Persistence} from "./Persistence";
|
||||
import {addOnLoad, q} from "./Shared";
|
||||
import {addOnLoad, ExpectedGoodbyeError, q} from "./Shared";
|
||||
import {Terminal} from "./Terminal";
|
||||
import * as semver from "semver";
|
||||
|
||||
|
||||
declare global {
|
||||
|
@ -19,13 +20,45 @@ declare global {
|
|||
}
|
||||
|
||||
|
||||
/**
|
||||
* Compares version numbers to ensure no compatibility errors ensure.
|
||||
*/
|
||||
addOnLoad(() => {
|
||||
const userVersion = Persistence.getVersion();
|
||||
const latestVersion = "%%VERSION_NUMBER%%";
|
||||
|
||||
if (semver.lt(userVersion, latestVersion)) {
|
||||
Persistence.reset();
|
||||
Persistence.setWasUpdated(true); // Message is displayed after reload
|
||||
location.reload();
|
||||
throw new ExpectedGoodbyeError("Goodbye");
|
||||
}
|
||||
|
||||
if (Persistence.getWasUpdated()) {
|
||||
q("#terminalOutput").innerHTML = "" +
|
||||
"<span style=\"color:red\">This website has been updated. To prevent unexpected errors, all previous " +
|
||||
"user changes have been reset.</span>\n\n";
|
||||
Persistence.setWasUpdated(false);
|
||||
}
|
||||
|
||||
Persistence.setVersion(latestVersion);
|
||||
});
|
||||
|
||||
/**
|
||||
* Exist the application if the server is "shut down".
|
||||
*/
|
||||
addOnLoad(() => {
|
||||
if (Persistence.getPoweroff()) {
|
||||
q("#terminalOutput").innerText = "Could not connect to fwdekker.com. Retrying in 10 seconds.";
|
||||
setTimeout(() => location.reload(), 10000);
|
||||
return;
|
||||
throw new ExpectedGoodbyeError("Goodbye");
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Initializes the application.
|
||||
*/
|
||||
addOnLoad(() => {
|
||||
window.terminal = new Terminal(
|
||||
q("#terminal"),
|
||||
q("#terminalCurrentFocusInput"),
|
||||
|
|
|
@ -10,51 +10,19 @@ import {UserList} from "./UserList";
|
|||
*/
|
||||
export class Persistence {
|
||||
/**
|
||||
* Deserializes an environment from persistent storage, or returns the default environment if the deserialization
|
||||
* failed.
|
||||
*
|
||||
* @param userList the list of users used to validate the `user` environment variable
|
||||
* Removes all persistent storage.
|
||||
*/
|
||||
static getEnvironment(userList: UserList): Environment {
|
||||
const environmentString = Cookies.get("env") ?? "{}";
|
||||
|
||||
let environment: Environment;
|
||||
try {
|
||||
environment = new Environment(["cwd", "home", "user", "status"], JSON.parse(environmentString));
|
||||
} catch (error) {
|
||||
console.warn("Failed to set environment from cookie.");
|
||||
environment = new Environment(["cwd", "home", "user", "status"]);
|
||||
}
|
||||
|
||||
// Check user in environment
|
||||
if (!environment.has("user")) {
|
||||
environment.set("user", "felix");
|
||||
} else if (environment.get("user") !== "" && !userList.has(environment.get("user"))) {
|
||||
console.warn(`Invalid user '${environment.get("user")}' in environment.`);
|
||||
environment.set("user", "felix");
|
||||
}
|
||||
|
||||
// Set home directory
|
||||
environment.set("home", userList.get(environment.get("user"))?.home ?? "/");
|
||||
|
||||
// Check cwd in environment
|
||||
if (!environment.has("cwd"))
|
||||
environment.set("cwd", environment.get("home"));
|
||||
|
||||
// Set status
|
||||
environment.set("status", "0");
|
||||
|
||||
return environment;
|
||||
static reset(): void {
|
||||
sessionStorage.clear();
|
||||
localStorage.clear();
|
||||
Cookies.remove("env");
|
||||
Cookies.remove("poweroff");
|
||||
}
|
||||
|
||||
/**
|
||||
* Persists the given environment.
|
||||
*
|
||||
* @param environment the environment to persist
|
||||
*/
|
||||
static setEnvironment(environment: Environment): void {
|
||||
Cookies.set("env", environment.variables, {"path": "/"});
|
||||
}
|
||||
|
||||
///
|
||||
/// Long-term storage
|
||||
///
|
||||
|
||||
/**
|
||||
* Deserializes a file system from persistent storage, or returns the default file system if the deserialization
|
||||
|
@ -108,21 +76,89 @@ export class Persistence {
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns the persisted "power off" setting.
|
||||
* Deserializes an environment from persistent storage, or returns the default environment if the deserialization
|
||||
* failed.
|
||||
*
|
||||
* @param userList the list of users used to validate the `user` environment variable
|
||||
*/
|
||||
static getEnvironment(userList: UserList): Environment {
|
||||
const environmentString = Cookies.get("env") ?? "{}";
|
||||
|
||||
let environment: Environment;
|
||||
try {
|
||||
environment = new Environment(["cwd", "home", "user", "status"], JSON.parse(environmentString));
|
||||
} catch (error) {
|
||||
console.warn("Failed to set environment from cookie.");
|
||||
environment = new Environment(["cwd", "home", "user", "status"]);
|
||||
}
|
||||
|
||||
// Check user in environment
|
||||
if (!environment.has("user")) {
|
||||
environment.set("user", "felix");
|
||||
} else if (environment.get("user") !== "" && !userList.has(environment.get("user"))) {
|
||||
console.warn(`Invalid user '${environment.get("user")}' in environment.`);
|
||||
environment.set("user", "felix");
|
||||
}
|
||||
|
||||
// Set home directory
|
||||
environment.set("home", userList.get(environment.get("user"))?.home ?? "/");
|
||||
|
||||
// Check cwd in environment
|
||||
if (!environment.has("cwd"))
|
||||
environment.set("cwd", environment.get("home"));
|
||||
|
||||
// Set status
|
||||
environment.set("status", "0");
|
||||
|
||||
return environment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Persists the given environment.
|
||||
*
|
||||
* @param environment the environment to persist
|
||||
*/
|
||||
static setEnvironment(environment: Environment): void {
|
||||
Cookies.set("env", environment.variables, {"path": "/"});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the version of the scripts that were used the last time the user visited the website.
|
||||
*/
|
||||
static getVersion(): string {
|
||||
return localStorage.getItem("version") ?? "%%VERSION_NUMBER%%";
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the version of the scripts that were used the last time the user visited the website.
|
||||
*
|
||||
* @param version the version of the scripts that were used the last time the user visited the website
|
||||
*/
|
||||
static setVersion(version: string) {
|
||||
localStorage.setItem("version", version);
|
||||
}
|
||||
|
||||
|
||||
///
|
||||
/// Short-term storage
|
||||
///
|
||||
|
||||
/**
|
||||
* Returns `true` if and only if the server is "turned off".
|
||||
*/
|
||||
static getPoweroff(): boolean {
|
||||
try {
|
||||
return JSON.parse(Cookies.get("poweroff") ?? "false");
|
||||
} catch(error) {
|
||||
} catch (error) {
|
||||
console.warn("Failed to deserialize 'poweroff' cookie.", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Persists the "power off" setting.
|
||||
* Stores whether the server is "turned off".
|
||||
*
|
||||
* @param value the value to persist for the "power off" setting
|
||||
* @param value whether the server is "turned off"
|
||||
*/
|
||||
static setPoweroff(value: boolean): void {
|
||||
Cookies.set("poweroff", "" + value, {
|
||||
|
@ -132,11 +168,23 @@ export class Persistence {
|
|||
}
|
||||
|
||||
/**
|
||||
* Removes all persistent storage.
|
||||
* Returns `true` if and only if the terminal was updated in this session.
|
||||
*/
|
||||
static reset(): void {
|
||||
localStorage.clear();
|
||||
Cookies.remove("env");
|
||||
Cookies.remove("poweroff");
|
||||
static getWasUpdated(): boolean {
|
||||
try {
|
||||
return JSON.parse(sessionStorage.getItem("has-updated") ?? "false");
|
||||
} catch (error) {
|
||||
console.warn("Failed to deserialize 'poweroff' cookie.", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores whether the terminal was updated in this session.
|
||||
*
|
||||
* @param value whether the terminal was updated in this session
|
||||
*/
|
||||
static setWasUpdated(value: boolean): void {
|
||||
sessionStorage.setItem("has-updated", "" + value);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -129,6 +129,18 @@ export function q(query: string): HTMLElement {
|
|||
}
|
||||
|
||||
|
||||
/**
|
||||
* Indicates that the application will exit under normal circumstances.
|
||||
*
|
||||
* That is, this is not actually an error. This "error" is thrown when the normal flow of execution should be
|
||||
* interrupted right away so that the application can exit.
|
||||
*/
|
||||
export class ExpectedGoodbyeError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates that an argument is given to a function that should not have been given.
|
||||
*
|
||||
|
|
|
@ -86,8 +86,8 @@ export class Terminal {
|
|||
scrollStartPosition = newPosition;
|
||||
}, {passive: true});
|
||||
|
||||
this.outputText = this.shell.generateHeader();
|
||||
this.prefixText = this.shell.generatePrefix();
|
||||
this.outputText += this.shell.generateHeader();
|
||||
this.prefixText += this.shell.generatePrefix();
|
||||
this.input.focus();
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue