Move index to /terminal

This commit is contained in:
2022-09-30 19:23:14 +02:00
parent aab77d8352
commit 995cfc5aea
13 changed files with 154 additions and 118 deletions

View File

@ -0,0 +1,120 @@
import type { NextPage } from "next";
import { useEffect, useRef, useState, isValidElement, useCallback } from "react";
import { useRouter } from "next/router";
import styles from "../../styles/ProjectModal.module.css";
import type { Project, Diary } from "../../lib/content/types";
import { useCommands } from "../../lib/commands/ContextProvider";
import { generateContent, projectEmpty } from "../../lib/content/generate";
import { useModalFunctions } from "./contexts/ModalFunctions";
import Spinner from "../Spinner";
import { renderToStaticMarkup } from "react-dom/server";
// Code Highlighting
import hljs from "highlight.js";
import rust from "highlight.js/lib/languages/rust";
import bash from "highlight.js/lib/languages/shell";
hljs.registerLanguage("rust", rust);
hljs.registerLanguage("bash", bash);
hljs.registerLanguage("console", bash);
hljs.registerLanguage("shell", bash);
const ProjectModal: NextPage = () => {
const [visible, _setVisible] = useState<boolean>(false);
const [currentContent, _setCurrentContent] = useState<Project | Diary | undefined>(undefined);
const [currentPage, setCurrentPage] = useState<number>(0);
const [HTMLContent, _setHTMLContent] = useState<string>(projectEmpty);
const router = useRouter();
const { updateCallbacks: updateCmdCallbacks, cmdContext } = useCommands();
const { updateCallbacks: updateModalCallbacks } = useModalFunctions();
const setHTMLContent = (html: any) => {
switch (true) {
case typeof html === "string": {
_setHTMLContent(html);
return;
}
case isValidElement(html): {
_setHTMLContent(renderToStaticMarkup(html));
return;
}
default: {
try {
_setHTMLContent(html.toString());
} catch {}
return;
}
}
};
const setModalContent = async (content: Project | Diary, selectedPage?: number) => {
_setCurrentContent(content);
router.replace("#", `#/${content.type}/${content.name}${content.type === "diary" && selectedPage ? `/${selectedPage}`: ""}`, {shallow: true});
if (content.type === "diary") setCurrentPage(selectedPage === undefined ? 0 : selectedPage);
setHTMLContent(<Spinner size={200} />);
setHTMLContent(await generateContent(content, selectedPage));
};
const setVisible = async (visible: boolean) => {
if (!visible) {
if (window) window.removeEventListener("hashchange", contentFromHash);
router.replace("#", undefined, {shallow: true});
} else {
if (window) window.addEventListener("hashchange", contentFromHash);
}
_setVisible(visible);
};
const contentFromHash = () => {
if (!window) return;
const selected = window.location.hash.split("/");
if (selected.length > 2) cmdContext.executeCommand(`project ${selected[2]}${selected[3] ? ` ${selected[3]}` : ""}`);
};
const onContentReady = () => {
contentFromHash();
};
updateCmdCallbacks({ setModalVisible: setVisible, setModalContent, setModalHTML: setHTMLContent });
updateModalCallbacks({ setVisible, setContent: setModalContent, setHtml: setHTMLContent, onContentReady });
useEffect(() => {
hljs.highlightAll();
}, [HTMLContent]);
const containerRef = useRef<HTMLDivElement>(null);
if (!visible) return <></>;
const nextPageSelector = (() => {
if (!currentContent || currentContent?.type !== "diary" || currentContent.entries.length === 0) return null;
const prev = <span className={styles.leftSelectSpace}>{currentPage > 0 ? <a className={styles.fakeLink} onClick={() => setModalContent(currentContent, currentPage - 1)}>&lt; {currentPage - 1 > 0 ? currentContent.entries[currentPage - 2].title : "Main page"}</a> : null}</span>;
const next = <span className={styles.rightSelectSpace}>{currentPage < currentContent.entries.length ? <a className={styles.fakeLink} onClick={() => setModalContent(currentContent, currentPage + 1)}>{currentContent.entries[currentPage].title} &gt;</a> : null}</span>;
const select = (
<select onChange={(e) => setModalContent(currentContent, Number.parseInt(e.target.value))} value={currentPage}>
<option key={-1} value={0}>Main page</option>
{currentContent.entries.map((entry, i) => <option key={i} value={i + 1}>{entry.title}</option>)}
</select>
);
return <div className={styles.pageSelector}>{prev}<span style={{visibility: currentPage > 0 ? "visible" : "hidden"}}>&nbsp;&nbsp;|&nbsp;&nbsp;</span>{select}<span style={{visibility: currentPage < currentContent.entries.length ? "visible" : "hidden"}}>&nbsp;&nbsp;|&nbsp;&nbsp;</span>{next}</div>;
})();
return <div className={styles.modal} onClick={() => setVisible(false)}>
<a onClick={() => setVisible(false)} className={styles.fakeLink}>
<div className={styles.modalClose}><div className={styles.modalCloseAlign}>X</div></div>
</a>
<div className={styles.modalContainer} onClick={(event) => event.stopPropagation()}>
{nextPageSelector}
<div className={`${styles.modalText} asciidoc`} ref={containerRef} dangerouslySetInnerHTML={{ __html: HTMLContent ? HTMLContent : projectEmpty }}>
</div>
{nextPageSelector}
</div>
</div>;
};
export default ProjectModal;

View File

@ -0,0 +1,101 @@
import { NextPage } from "next";
import Link from "next/link";
import type { BaseSyntheticEvent, MutableRefObject } from "react";
import styles from "../../../styles/REPL/REPLHistory.module.css";
interface REPLHistoryParams {
history: string[];
inputRef: MutableRefObject<HTMLInputElement|null>;
}
const REPLHistory: NextPage<REPLHistoryParams> = ({history, inputRef}) => {
const focusInput = () => {if (inputRef.current) inputRef.current.focus();};
const forceInput = (e: BaseSyntheticEvent) => {
const command = (e.target as HTMLSpanElement).innerHTML;
if (inputRef.current) {
inputRef.current.value = command;
// TODO
// Fix this as this currently doesn't work
inputRef.current.dispatchEvent(new KeyboardEvent("keydown", {
key: "Enter",
keyCode: 13,
}));
}
return true;
};
const makeLinks = (line: string) => {
let idxStart = line.indexOf("#{");
let idxSep = line.indexOf("|", idxStart);
let idxEnd = line.indexOf("}", idxSep);
if (idxStart === -1 || idxSep === -1 || idxEnd === -1) return [line];
const result = [];
while (idxStart !== -1 && idxSep !== -1 && idxEnd !== -1) {
const linkText = line.substring(idxStart+2, idxSep);
const linkHref = line.substring(idxSep+1, idxEnd);
const isLocal = linkHref.startsWith("https://c0ntroller.de") || linkHref.startsWith("/") || linkHref.startsWith("#");
result.push(line.substring(0, idxStart));
result.push(<Link href={linkHref} key={`${linkHref}${line.length}`}><a className={styles.link} target={isLocal ? "_self" : "_blank"} rel={isLocal ? "" : "noreferrer"}>{linkText}</a></Link>);
line = line.substring(idxEnd+1);
idxStart = line.indexOf("#{");
idxSep = line.indexOf("|", idxStart);
idxEnd = line.indexOf("}", idxSep);
}
// Its already cut off
result.push(line);
return result;
};
const makeCommands = (line: string|JSX.Element) => {
if (typeof line !== "string") return line;
let idxStart = line.indexOf("%{");
let idxEnd = line.indexOf("}", idxStart);
if (idxStart === -1 || idxEnd === -1) return line;
const result = [];
while (idxStart !== -1 && idxEnd !== -1) {
const cmdText = line.substring(idxStart+2, idxEnd);
result.push(line.substring(0, idxStart));
result.push(<span className={styles.cmd} onClick={forceInput} key={`${cmdText}${line.length}${cmdText}`}>{cmdText}</span>);
line = line.substring(idxEnd+1);
idxStart = line.indexOf("%{");
idxEnd = line.indexOf("}", idxStart);
}
// Its already cut off
result.push(line);
return result;
};
const parseLine = (line: string) => {
if (line === "") return "\u00A0";
const resultLinks = makeLinks(line);
const resultAll = resultLinks.map(makeCommands);
return resultAll.flat();
};
return <div className={styles.container} onClick={focusInput}>
{ history.map((value, idx) => <div className={styles.line} key={`${idx}${value}`}>
{parseLine(value)}
</div>)
}
{<noscript>
<div className={styles.line}>JavaScript must be enabled, else this site won&apos;t work.</div>
<div className={styles.line}>This site doesn&apos;t use any trackers, so please enable JS!</div>
</noscript>}
</div>;
};
export default REPLHistory;

View File

@ -0,0 +1,169 @@
import type { NextPage } from "next";
import { MutableRefObject, useState, createRef, useEffect } from "react";
import { CommandInterface } from "../../../lib/commands";
import styles from "../../../styles/REPL/REPLInput.module.css";
import { useCommands } from "../../../lib/commands/ContextProvider";
import { useModalFunctions } from "../contexts/ModalFunctions";
interface REPLInputParams {
historyCallback: CallableFunction;
historyClear: CallableFunction;
inputRef: MutableRefObject<HTMLInputElement|null>;
}
const REPLInput: NextPage<REPLInputParams> = ({historyCallback, historyClear, inputRef}) => {
const typed = createRef<HTMLSpanElement>();
const completion = createRef<HTMLSpanElement>();
const [currentCmd, setCurrentCmd] = useState<string[]>([]);
let [justTabbed, setJustTabbed] = useState<number>(0); // Because setters are not in sync but the events are too fast
const [inCmdHistory, setInCmdHistory] = useState<number>(-1);
const [cmdHistory, setCmdHistory] = useState<string[]>([]);
const [usrInputTmp, setUsrInputTmp] = useState<string>("");
const {cmdContext: cmdIf, updateCallbacks} = useCommands();
const { modalFunctions } = useModalFunctions();
updateCallbacks({ getCmdHistory: () => cmdHistory });
const setInput = (inputRef: HTMLInputElement, input: string) => {
const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value")?.set;
if (!nativeSetter) return;
nativeSetter.call(inputRef, input);
//if(typed.current) typed.current.innerHTML = input;
//if(completion.current) completion.current.innerHTML = "";
inputRef.dispatchEvent(new Event("input", { bubbles: true }));
inputRef.dispatchEvent(new Event("change", { bubbles: true, }));
};
const clearInput = (inputRef: HTMLInputElement) => {
setInput(inputRef, "");
};
const replinOnChange = (e: React.FormEvent<HTMLInputElement>) => {
const input = (e.target as HTMLInputElement);
const currentInput = input.value.toLowerCase();
// Force lowercase
input.value = currentInput;
if (currentInput.trim() === "") input.value = "";
if (currentInput.includes(" ")) {
// Command already typed
input.maxLength = 524288; // Default value
if (typed.current) typed.current.innerHTML = "";
if (completion.current) completion.current.innerHTML = "";
setCurrentCmd([]);
return;
} else {
input.maxLength = 20;
if(justTabbed === 0) {
// Get completion hint
const suggest = CommandInterface.commandCompletion(currentInput);
setCurrentCmd(suggest);
if (suggest.length === 0) suggest.push("");
if (typed.current) typed.current.innerHTML = currentInput;
if (completion.current) completion.current.innerHTML = suggest[0].substring(currentInput.length);
} else {
if (typed.current) typed.current.innerHTML = "";
if (completion.current) completion.current.innerHTML = "";
}
}
};
const keyEvent = (e: React.KeyboardEvent<HTMLInputElement>) => {
const input = (e.target as HTMLInputElement);
if (e.key === "Tab") e.preventDefault();
if (e.key === "Tab" && currentCmd.length !== 0) {
const cmd = `${currentCmd[justTabbed % currentCmd.length]}${currentCmd.length === 1 ? " " : ""}`;
setJustTabbed(justTabbed + 1);
justTabbed += 1; // Because setters are not in sync but the events are too fast
setInput(input, cmd);
return false;
} else setJustTabbed(0);
switch (true) {
case e.key === "Enter": {
e.preventDefault();
const command = (e.target as HTMLInputElement).value.trim();
if (command.length === 0) {
historyCallback(["$"]);
return;
}
if (cmdHistory.at(-1) !== command) setCmdHistory(cmdHistory.concat([command]).splice(0, 100));
clearInput(input);
setInCmdHistory(-1);
setCurrentCmd([]);
if (command === "clear") {
historyClear();
return false;
}
const result = cmdIf.executeCommand(command);
historyCallback(result);
return false;
}
case e.key === "d" && e.ctrlKey: {
e.preventDefault();
const result = cmdIf.executeCommand("exit");
clearInput(input);
historyCallback(result);
return false;
}
case e.key === "l" && e.ctrlKey: {
e.preventDefault();
clearInput(input);
historyClear();
return false;
}
case (e.key === "c" || e.key === "u") && e.ctrlKey: {
e.preventDefault();
clearInput(input);
return false;
}
case e.key === "ArrowUp": {
e.preventDefault();
const idx = inCmdHistory + 1;
if (idx < cmdHistory.length) {
if (inCmdHistory === -1) setUsrInputTmp(input.value);
const cmd = cmdHistory[cmdHistory.length - idx - 1];
setInput(input, cmd);
setInCmdHistory(idx);
}
return false;
}
case e.key === "ArrowDown": {
e.preventDefault();
const idx = inCmdHistory - 1;
if (0 <= idx) {
const cmd = cmdHistory[cmdHistory.length - idx - 1];
setInput(input, cmd);
setInCmdHistory(idx);
} else if (idx === -1) {
setInput(input, usrInputTmp);
setInCmdHistory(-1);
}
return false;
}
}
};
useEffect(() => {
if (!window || !cmdIf) return;
const color = window.localStorage.getItem("color");
if(color) cmdIf.executeCommand(`color ${color}`);
const history = window.localStorage.getItem("history");
try {
if (history) setCmdHistory(JSON.parse(history));
} catch {}
}, [cmdIf]);
return <div className={styles.wrapperwrapper}>
<span className={styles.inputstart}>$&nbsp;</span>
<div className={styles.wrapper}>
<input ref={inputRef as MutableRefObject<HTMLInputElement>} className={styles.in} type={"text"} onChange={replinOnChange} onKeyDown={keyEvent} spellCheck={"false"} autoComplete={"off"} autoFocus maxLength={20} />
<span className={styles.completionWrapper}><span ref={typed} className={styles.typed}></span><span ref={completion} className={styles.completion}></span></span>
</div>
</div>;
};
export default REPLInput;

View File

@ -0,0 +1,35 @@
import { MutableRefObject, useEffect, useRef, useState } from "react";
import REPLInput from "./REPLInput";
import REPLHistory from "./REPLHistory";
import styles from "../../../styles/REPL/REPLComplete.module.css";
import type { NextPage } from "next";
import { useCommands } from "../../../lib/commands/ContextProvider";
interface IREPLProps {
inputRef: MutableRefObject<HTMLInputElement|null>;
buildTime: string;
}
const REPL: NextPage<IREPLProps> = ({ inputRef, buildTime }) => {
const [history, manipulateHistory] = useState<string[]>([`cer0 0S - Build ${buildTime}`]);
const containerRef = useRef<HTMLDivElement>(null);
const onCommandExecuted = (result: string[]) => manipulateHistory(result.reverse().concat(history).slice(0, 1000));
const onClearHistory = () => manipulateHistory([]);
const focusInput = () => {
if (inputRef.current) inputRef.current.focus();
};
useEffect(() => {
if(containerRef && containerRef.current) containerRef.current.scrollTo(0, containerRef.current.scrollHeight);
}, [history]);
return (<div className={styles.container} ref={containerRef}>
<REPLHistory history={history} inputRef={inputRef} />
<REPLInput historyCallback={onCommandExecuted} historyClear={onClearHistory} inputRef={inputRef} />
<div style={{flexGrow: 2}} onClick={focusInput}></div>
</div>);
};
export default REPL;

View File

@ -0,0 +1,18 @@
import { createContext, useContext } from "react";
import type { PropsWithChildren } from "react";
import type { Project, Diary } from "../../lib/content/types";
interface ModalFunctions {
setVisible?: CallableFunction;
setContent?: (content: Project| Diary) => void;
setHtml?: (html: any) => void;
onContentReady?: () => void;
}
const modalFunctions: ModalFunctions = {};
const ModalContext = createContext(modalFunctions);
const updateCallbacks = (callbacks: ModalFunctions) => Object.assign(modalFunctions, callbacks);
const useModalFunctions = () => ({modalFunctions: useContext(ModalContext), updateCallbacks});
const ModalFunctionProvider = (props: PropsWithChildren<{}>) => <ModalContext.Provider value={modalFunctions} {...props} />;
export { ModalFunctionProvider, useModalFunctions };