Move index to /terminal
This commit is contained in:
120
components/Terminal/ProjectModal.tsx
Normal file
120
components/Terminal/ProjectModal.tsx
Normal 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)}>< {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} ></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"}}> | </span>{select}<span style={{visibility: currentPage < currentContent.entries.length ? "visible" : "hidden"}}> | </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;
|
101
components/Terminal/REPL/REPLHistory.tsx
Normal file
101
components/Terminal/REPL/REPLHistory.tsx
Normal 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't work.</div>
|
||||
<div className={styles.line}>This site doesn't use any trackers, so please enable JS!</div>
|
||||
</noscript>}
|
||||
</div>;
|
||||
};
|
||||
|
||||
export default REPLHistory;
|
169
components/Terminal/REPL/REPLInput.tsx
Normal file
169
components/Terminal/REPL/REPLInput.tsx
Normal 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}>$ </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;
|
35
components/Terminal/REPL/index.tsx
Normal file
35
components/Terminal/REPL/index.tsx
Normal 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;
|
18
components/Terminal/contexts/ModalFunctions.tsx
Normal file
18
components/Terminal/contexts/ModalFunctions.tsx
Normal 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 };
|
Reference in New Issue
Block a user