Move index to /terminal
This commit is contained in:
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;
|
Reference in New Issue
Block a user