forked from tools/josh
parent
57bcfcbf0f
commit
13d2164e89
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "fwdekker.com",
|
||||
"version": "0.39.20",
|
||||
"version": "0.40.0",
|
||||
"description": "The source code of [my personal website](https://fwdekker.com/).",
|
||||
"author": "Felix W. Dekker",
|
||||
"browser": "dist/bundle.js",
|
||||
|
|
|
@ -1,21 +1,21 @@
|
|||
a {
|
||||
#terminal a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:link, a:visited {
|
||||
#terminal a:link, #terminal a:visited {
|
||||
color: #00FF00;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
#terminal a:hover {
|
||||
color: #00BF00;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
a:link.dirLink, a:visited.dirLink {
|
||||
#terminal a:link.dirLink, #terminal a:visited.dirLink {
|
||||
color: #00FF00;
|
||||
}
|
||||
|
||||
a:link.fileLink, a:visited.fileLink {
|
||||
#terminal a:link.fileLink, #terminal a:visited.fileLink {
|
||||
color: #FFFF00;
|
||||
}
|
||||
|
||||
|
@ -51,18 +51,11 @@ a:link.fileLink, a:visited.fileLink {
|
|||
|
||||
body {
|
||||
background-color: black;
|
||||
overscroll-behavior-y: contain;
|
||||
}
|
||||
|
||||
#terminal {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
|
||||
min-height: 100%;
|
||||
padding: 25px;
|
||||
overflow: hidden;
|
||||
|
||||
cursor: text;
|
||||
color: white;
|
||||
|
@ -107,3 +100,9 @@ body {
|
|||
.errorMessage {
|
||||
color: #FF3333;
|
||||
}
|
||||
|
||||
|
||||
#nav {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
<link rel="apple-touch-icon" href="icon_ios.png?v=%%VERSION_NUMBER%%" />
|
||||
<link rel="manifest" href="manifest.json?v=%%VERSION_NUMBER%%">
|
||||
|
||||
<link rel="stylesheet" href="https://static.fwdekker.com/lib/template/2.x.x/template.css" />
|
||||
<link rel="stylesheet" href="https://static.fwdekker.com/fonts/roboto-mono/roboto-mono.css" />
|
||||
<!--suppress HtmlUnknownTarget -->
|
||||
<link rel="stylesheet" href="main.css?v=%%VERSION_NUMBER%%" />
|
||||
|
@ -28,6 +29,8 @@
|
|||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<div id="nav"></div>
|
||||
|
||||
<!-- Comment out newlines and indents because of `white-space: pre-wrap` in CSS. -->
|
||||
<div id="terminal"><!--
|
||||
--><noscript><!--
|
||||
|
@ -54,17 +57,18 @@
|
|||
|
||||
<script>
|
||||
if (/MSIE|Trident/.test(window.navigator.userAgent)) {
|
||||
window.onload = function () {
|
||||
window.onload = function() {
|
||||
document.getElementById("ie-warning").style.display = "block";
|
||||
};
|
||||
}
|
||||
|
||||
if ("serviceWorker" in navigator) {
|
||||
window.addEventListener("load", function () {
|
||||
window.addEventListener("load", function() {
|
||||
return navigator.serviceWorker.register("sw.js?v=%%VERSION_NUMBER%%");
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<script src="https://static.fwdekker.com/lib/template/2.x.x/template.js"></script>
|
||||
<!--suppress HtmlUnknownTarget -->
|
||||
<script type="module" src="bundle.js?v=%%VERSION_NUMBER%%"></script>
|
||||
</body>
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import * as semver from "semver";
|
||||
// @ts-ignore
|
||||
const {$, doAfterLoad, nav} = window.fwdekker;
|
||||
import {FileSystem} from "./FileSystem";
|
||||
import {Persistence} from "./Persistence";
|
||||
import {addOnLoad, ExpectedGoodbyeError, q} from "./Shared";
|
||||
import {ExpectedGoodbyeError} from "./Shared";
|
||||
import {Terminal} from "./Terminal";
|
||||
|
||||
|
||||
|
@ -24,7 +26,7 @@ declare global {
|
|||
/**
|
||||
* Compares version numbers to ensure no compatibility errors ensue.
|
||||
*/
|
||||
addOnLoad(() => {
|
||||
doAfterLoad(() => {
|
||||
const userVersion = Persistence.getVersion();
|
||||
const latestVersion = "%%VERSION_NUMBER%%";
|
||||
|
||||
|
@ -36,7 +38,7 @@ addOnLoad(() => {
|
|||
}
|
||||
|
||||
if (Persistence.getWasUpdated()) {
|
||||
q("#terminalOutput").innerHTML = "" +
|
||||
$("#terminalOutput").innerHTML = "" +
|
||||
"<span style=\"color:red\">The terminal application has been updated. To prevent unexpected errors, all " +
|
||||
"previous user changes have been reset.</span>\n\n";
|
||||
Persistence.setWasUpdated(false);
|
||||
|
@ -48,10 +50,10 @@ addOnLoad(() => {
|
|||
/**
|
||||
* Exits the application if the server is "shut down".
|
||||
*/
|
||||
addOnLoad(() => {
|
||||
doAfterLoad(() => {
|
||||
if (!Persistence.getPoweroff()) return;
|
||||
|
||||
q("#terminalOutput").innerText = "Could not connect to fwdekker.com. Retrying in 10 seconds.";
|
||||
$("#terminalOutput").innerText = "Could not connect to fwdekker.com. Retrying in 10 seconds.";
|
||||
setTimeout(() => location.reload(), 10000);
|
||||
throw new ExpectedGoodbyeError("Goodbye");
|
||||
});
|
||||
|
@ -59,16 +61,17 @@ addOnLoad(() => {
|
|||
/**
|
||||
* Initializes the application.
|
||||
*/
|
||||
addOnLoad(async () => {
|
||||
doAfterLoad(async () => {
|
||||
$("#nav").appendChild(nav("/"));
|
||||
if (!Persistence.hasFileSystem())
|
||||
await FileSystem.loadNavApi();
|
||||
|
||||
window.terminal = new Terminal(
|
||||
q("#terminal"),
|
||||
q("#terminalInputField"),
|
||||
q("#terminalOutput"),
|
||||
q("#terminalInputPrefix"),
|
||||
q("#terminalSuggestions")
|
||||
$("#terminal"),
|
||||
$("#terminalInputField"),
|
||||
$("#terminalOutput"),
|
||||
$("#terminalInputPrefix"),
|
||||
$("#terminalSuggestions")
|
||||
);
|
||||
window.execute = (command: string) => window.terminal.processInput(command);
|
||||
|
||||
|
|
|
@ -24,22 +24,6 @@ export const asciiHeaderHtml =
|
|||
export const emptyFunction = () => {};
|
||||
|
||||
|
||||
/**
|
||||
* Runs the given function as soon as the page is done loading by "appending" it to the current definition of
|
||||
* `window.onload`.
|
||||
*
|
||||
* @param fun the function to run as soon as the page is done loading
|
||||
*/
|
||||
export function addOnLoad(fun: () => void): void {
|
||||
const oldOnLoad = window.onload ?? emptyFunction;
|
||||
|
||||
window.onload = () => {
|
||||
// @ts-ignore: Call works without parameters as well
|
||||
oldOnLoad();
|
||||
fun();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces all special HTML characters with escaped variants.
|
||||
*
|
||||
|
@ -122,41 +106,6 @@ export function moveCaretToEndOf(node: Node | null): void {
|
|||
moveCaretTo(node, (node?.textContent ?? "").length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of pixels in a CSS value that describes a number of pixels, or `0` if the given string is `null`
|
||||
* or does not contain a number.
|
||||
*
|
||||
* For example, if the given string is `"3px"`, this function will return `3`.
|
||||
*
|
||||
* @param string the CSS value to extract the number of pixels from
|
||||
* @throws if the given string does not end with the text `"px"`
|
||||
*/
|
||||
export function parseCssPixels(string: string | null): number {
|
||||
if (string === null || string.trim() === "") {
|
||||
return 0;
|
||||
} else {
|
||||
if (!string.endsWith("px"))
|
||||
throw new IllegalArgumentError("CSS string is not expressed in pixels.");
|
||||
|
||||
const result = parseFloat(string);
|
||||
return isNaN(result) ? 0 : result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Type-safe shorthand for `document.querySelector(query)`.
|
||||
*
|
||||
* @param query the query to run
|
||||
* @throws if the element could not be found
|
||||
*/
|
||||
export function q(query: string): HTMLElement {
|
||||
const element = document.querySelector(query);
|
||||
if (!(element instanceof HTMLElement))
|
||||
throw `Could not find element \`${query}\`.`;
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the longest common prefix of the given strings, or `undefined` if an empty array is given.
|
||||
*
|
||||
|
|
|
@ -6,8 +6,7 @@ import {
|
|||
findLongestCommonPrefix,
|
||||
isStandalone,
|
||||
moveCaretTo,
|
||||
moveCaretToEndOf,
|
||||
parseCssPixels
|
||||
moveCaretToEndOf
|
||||
} from "./Shared";
|
||||
import {Shell} from "./Shell";
|
||||
import {Buffer, StreamSet} from "./Stream";
|
||||
|
@ -17,11 +16,6 @@ import {Buffer, StreamSet} from "./Stream";
|
|||
* A terminal session that has input and output.
|
||||
*/
|
||||
export class Terminal {
|
||||
/**
|
||||
* The height of a single line in the output.
|
||||
*/
|
||||
private readonly lineHeight: number = 21; // TODO Calculate this dynamically
|
||||
|
||||
/**
|
||||
* The HTML element of the terminal.
|
||||
*/
|
||||
|
@ -108,21 +102,6 @@ export class Terminal {
|
|||
document.addEventListener("keydown", this.onkeydown.bind(this));
|
||||
this.input.addEventListener("input", () => this.suggestionsText = "");
|
||||
|
||||
let scrollStartPosition: number = 0;
|
||||
this.terminal.addEventListener("wheel", (event: WheelEvent) => {
|
||||
this.scroll -= Math.sign(event.deltaY);
|
||||
}, {passive: true});
|
||||
this.terminal.addEventListener("touchstart", (event: TouchEvent) => {
|
||||
scrollStartPosition = event.changedTouches[0].clientY;
|
||||
}, {passive: true});
|
||||
this.terminal.addEventListener("touchmove", (event: TouchEvent) => {
|
||||
const newPosition = event.changedTouches[0].clientY;
|
||||
const diff = scrollStartPosition - newPosition;
|
||||
|
||||
this.scroll -= diff / this.lineHeight;
|
||||
scrollStartPosition = newPosition;
|
||||
}, {passive: true});
|
||||
|
||||
this.outputText += this.shell.generateHeader();
|
||||
this.prefixText += this.shell.generatePrefix();
|
||||
this.input.focus();
|
||||
|
@ -187,34 +166,6 @@ export class Terminal {
|
|||
this.suggestions.innerHTML = suggestionsText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns how many lines the user has scrolled up in the terminal.
|
||||
*/
|
||||
private get scroll(): number {
|
||||
return -parseCssPixels(this.terminal.style.marginBottom) / this.lineHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the absolute number of lines to scroll up in the terminal relative to the bottom of the terminal.
|
||||
*
|
||||
* @param lines the absolute number of lines to scroll up in the terminal relative to the bottom of the terminal
|
||||
*/
|
||||
private set scroll(lines: number) {
|
||||
const screenHeight = document.documentElement.clientHeight
|
||||
- 2 * parseCssPixels(getComputedStyle(this.terminal).paddingTop); // top and bottom padding
|
||||
const linesFitOnScreen = Math.round(screenHeight / this.lineHeight);
|
||||
const linesInHistory = Math.round(this.output.offsetHeight / this.lineHeight) + 1; // +1 for input line
|
||||
|
||||
if (lines < 0)
|
||||
lines = 0;
|
||||
else if (linesInHistory <= linesFitOnScreen)
|
||||
lines = 0;
|
||||
else if (lines > linesInHistory - linesFitOnScreen)
|
||||
lines = linesInHistory - linesFitOnScreen;
|
||||
|
||||
this.terminal.style.marginBottom = (-lines * this.lineHeight) + "px";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if and only if the input field does not display the user's input.
|
||||
*/
|
||||
|
@ -288,7 +239,7 @@ export class Terminal {
|
|||
this.outputText += buffer;
|
||||
|
||||
this.prefixText = this.shell.generatePrefix();
|
||||
this.scroll = 0;
|
||||
this.input.scrollIntoView({behavior: "smooth"});
|
||||
}
|
||||
|
||||
|
||||
|
@ -351,7 +302,7 @@ export class Terminal {
|
|||
case "os":
|
||||
case "shift":
|
||||
// Do nothing
|
||||
return; // Return without scrolling to 0
|
||||
return;
|
||||
case "arrowup": {
|
||||
// Display previous entry from history
|
||||
this.inputText = this.inputHistory.previous();
|
||||
|
@ -387,7 +338,7 @@ export class Terminal {
|
|||
// Only if focused on the input as to not prevent copying of selected text
|
||||
if (event.ctrlKey) {
|
||||
if (this.input !== document.activeElement)
|
||||
return; // Return without scrolling to 0
|
||||
return;
|
||||
|
||||
this.ignoreInput();
|
||||
event.preventDefault();
|
||||
|
@ -421,7 +372,7 @@ export class Terminal {
|
|||
break;
|
||||
}
|
||||
|
||||
this.scroll = 0;
|
||||
this.input.scrollIntoView({behavior: "smooth"});
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import {expect} from "chai";
|
||||
import "mocha";
|
||||
|
||||
import {escapeHtml, extractWordBefore, getFileExtension, parseCssPixels} from "../main/js/Shared";
|
||||
import {escapeHtml, extractWordBefore, getFileExtension} from "../main/js/Shared";
|
||||
|
||||
|
||||
describe("shared functions", () => {
|
||||
|
@ -73,40 +73,6 @@ describe("shared functions", () => {
|
|||
expect(getFileExtension("fi.le.ext")).to.equal("ext");
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseCssPixels", () => {
|
||||
it("returns 0 if null is given", () => {
|
||||
expect(parseCssPixels(null)).to.equal(0);
|
||||
});
|
||||
|
||||
it("returns 0 if an empty string is given", () => {
|
||||
expect(parseCssPixels("")).to.equal(0);
|
||||
});
|
||||
|
||||
it("returns 0 if a string containing only whitespace is given", () => {
|
||||
expect(parseCssPixels(" ")).to.equal(0);
|
||||
});
|
||||
|
||||
it("throws an error if the string does not end with 'px'", () => {
|
||||
expect(() => parseCssPixels("12py")).to.throw();
|
||||
});
|
||||
|
||||
it("returns 0 if the string does not contain a number", () => {
|
||||
expect(parseCssPixels("errorpx")).to.equal(0);
|
||||
});
|
||||
|
||||
it("returns the number contained in the string", () => {
|
||||
expect(parseCssPixels("29px")).to.equal(29);
|
||||
});
|
||||
|
||||
it("returns the number contained in the string even if surrounded with whitespace", () => {
|
||||
expect(parseCssPixels(" 17 px")).to.equal(17);
|
||||
});
|
||||
|
||||
it("returns a decimal number", () => {
|
||||
expect(parseCssPixels("12.34px")).to.equal(12.34);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("extension functions", () => {
|
||||
|
|
Loading…
Reference in New Issue