feat: implement terminal engine with command dispatch and input handling
This commit is contained in:
+451
@@ -0,0 +1,451 @@
|
|||||||
|
import command from "../config.json" assert { type: "json" };
|
||||||
|
import { HELP } from "./commands/help";
|
||||||
|
import { BANNER } from "./commands/banner";
|
||||||
|
import { ABOUT } from "./commands/about";
|
||||||
|
import { DEFAULT } from "./commands/default";
|
||||||
|
import { createWhoami } from "./commands/whoami";
|
||||||
|
import config from "../config.json";
|
||||||
|
|
||||||
|
let mutWriteLines = document.getElementById("write-lines");
|
||||||
|
let historyIdx = 0;
|
||||||
|
let tempInput = "";
|
||||||
|
let userInput: string;
|
||||||
|
let isRoot = false;
|
||||||
|
let isPasswordInput = false;
|
||||||
|
let passwordCounter = 0;
|
||||||
|
let bareMode = false;
|
||||||
|
|
||||||
|
const WRITELINESCOPY = mutWriteLines;
|
||||||
|
const TERMINAL = document.getElementById("terminal");
|
||||||
|
const USERINPUT = document.getElementById("user-input") as HTMLInputElement;
|
||||||
|
const INPUT_HIDDEN = document.getElementById("input-hidden");
|
||||||
|
const PASSWORD = document.getElementById("password-input");
|
||||||
|
const PASSWORD_INPUT = document.getElementById(
|
||||||
|
"password-field"
|
||||||
|
) as HTMLInputElement;
|
||||||
|
const PRE_HOST = document.getElementById("pre-host");
|
||||||
|
const PRE_USER = document.getElementById("pre-user");
|
||||||
|
const HOST = document.getElementById("host");
|
||||||
|
const USER = document.getElementById("user");
|
||||||
|
const PROMPT = document.getElementById("prompt");
|
||||||
|
const COMMANDS = [
|
||||||
|
"help",
|
||||||
|
"about",
|
||||||
|
"whoami",
|
||||||
|
"banner",
|
||||||
|
"clear",
|
||||||
|
"key",
|
||||||
|
"linkedin",
|
||||||
|
"youtube",
|
||||||
|
"nagini",
|
||||||
|
"email",
|
||||||
|
];
|
||||||
|
const HISTORY: string[] = [];
|
||||||
|
const ROOT_PASSWORD = command.password;
|
||||||
|
|
||||||
|
const scrollToBottom = () => {
|
||||||
|
const MAIN = document.getElementById("main");
|
||||||
|
if (!MAIN) return;
|
||||||
|
|
||||||
|
MAIN.scrollTop = MAIN.scrollHeight;
|
||||||
|
};
|
||||||
|
|
||||||
|
function userInputHandler(e: KeyboardEvent) {
|
||||||
|
const key = e.key;
|
||||||
|
|
||||||
|
switch (key) {
|
||||||
|
case "Enter":
|
||||||
|
e.preventDefault();
|
||||||
|
if (!isPasswordInput) {
|
||||||
|
enterKey();
|
||||||
|
} else {
|
||||||
|
passwordHandler();
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollToBottom();
|
||||||
|
break;
|
||||||
|
case "Escape":
|
||||||
|
USERINPUT.value = "";
|
||||||
|
break;
|
||||||
|
case "ArrowUp":
|
||||||
|
arrowKeys(key);
|
||||||
|
e.preventDefault();
|
||||||
|
break;
|
||||||
|
case "ArrowDown":
|
||||||
|
arrowKeys(key);
|
||||||
|
break;
|
||||||
|
case "Tab":
|
||||||
|
tabKey();
|
||||||
|
e.preventDefault();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function enterKey() {
|
||||||
|
if (!mutWriteLines || !PROMPT) return;
|
||||||
|
const resetInput = "";
|
||||||
|
let newUserInput;
|
||||||
|
userInput = USERINPUT.value;
|
||||||
|
|
||||||
|
if (bareMode) {
|
||||||
|
newUserInput = userInput;
|
||||||
|
} else {
|
||||||
|
newUserInput = `<span class='output'>${userInput}</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
HISTORY.push(userInput);
|
||||||
|
historyIdx = HISTORY.length;
|
||||||
|
|
||||||
|
if (userInput === "clear") {
|
||||||
|
commandHandler(userInput.toLowerCase().trim());
|
||||||
|
USERINPUT.value = resetInput;
|
||||||
|
userInput = resetInput;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const div = document.createElement("div");
|
||||||
|
div.innerHTML = `<span id="prompt">${PROMPT.innerHTML}</span> ${newUserInput}`;
|
||||||
|
|
||||||
|
if (mutWriteLines.parentNode) {
|
||||||
|
mutWriteLines.parentNode.insertBefore(div, mutWriteLines);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userInput.trim().length !== 0) {
|
||||||
|
commandHandler(userInput.toLowerCase().trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
USERINPUT.value = resetInput;
|
||||||
|
userInput = resetInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
function tabKey() {
|
||||||
|
let currInput = USERINPUT.value;
|
||||||
|
|
||||||
|
for (const ele of COMMANDS) {
|
||||||
|
if (ele.startsWith(currInput)) {
|
||||||
|
USERINPUT.value = ele;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function arrowKeys(e: string) {
|
||||||
|
switch (e) {
|
||||||
|
case "ArrowDown":
|
||||||
|
if (historyIdx !== HISTORY.length) {
|
||||||
|
historyIdx += 1;
|
||||||
|
USERINPUT.value = HISTORY[historyIdx];
|
||||||
|
if (historyIdx === HISTORY.length) USERINPUT.value = tempInput;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "ArrowUp":
|
||||||
|
if (historyIdx === HISTORY.length) tempInput = USERINPUT.value;
|
||||||
|
if (historyIdx !== 0) {
|
||||||
|
historyIdx -= 1;
|
||||||
|
USERINPUT.value = HISTORY[historyIdx];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function commandHandler(input: string) {
|
||||||
|
if (input.startsWith("rm -rf") && input.trim() !== "rm -rf") {
|
||||||
|
if (isRoot) {
|
||||||
|
if (input === "rm -rf /" && !bareMode) {
|
||||||
|
bareMode = true;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!TERMINAL || !WRITELINESCOPY) return;
|
||||||
|
TERMINAL.innerHTML = "";
|
||||||
|
TERMINAL.appendChild(WRITELINESCOPY);
|
||||||
|
mutWriteLines = WRITELINESCOPY;
|
||||||
|
});
|
||||||
|
|
||||||
|
easterEggStyles();
|
||||||
|
setTimeout(() => {
|
||||||
|
writeLines(["What made you think that was a good idea?", "<br>"]);
|
||||||
|
}, 200);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
writeLines(["Now everything is ruined.", "<br>"]);
|
||||||
|
}, 1200);
|
||||||
|
} else if (input === "rm -rf /" && bareMode) {
|
||||||
|
writeLines(["there's no more / folder.", "<br>"]);
|
||||||
|
} else {
|
||||||
|
if (bareMode) {
|
||||||
|
writeLines(["What else are you trying to delete?", "<br>"]);
|
||||||
|
} else {
|
||||||
|
writeLines([
|
||||||
|
"<br>",
|
||||||
|
"Directory not found.",
|
||||||
|
"type <span class='command'>'ls'</span> for a list of directories.",
|
||||||
|
"<br>",
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
writeLines(["Permission not granted.", "<br>"]);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (input) {
|
||||||
|
case "clear":
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!TERMINAL || !WRITELINESCOPY) return;
|
||||||
|
TERMINAL.innerHTML = "";
|
||||||
|
TERMINAL.appendChild(WRITELINESCOPY);
|
||||||
|
mutWriteLines = WRITELINESCOPY;
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "pwd":
|
||||||
|
writeLines(["/home/guest", "<br>"]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "banner":
|
||||||
|
writeLines(bareMode ? ["Joshua SystemV", "<br>"] : BANNER);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "help":
|
||||||
|
writeLines(
|
||||||
|
bareMode
|
||||||
|
? ["maybe restarting your browser will fix this.", "<br>"]
|
||||||
|
: HELP
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "whoami":
|
||||||
|
writeLines(bareMode ? [`${command.username}`, "<br>"] : createWhoami());
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "about":
|
||||||
|
writeLines(bareMode ? ["Nothing to see here.", "<br>"] : ABOUT);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "key":
|
||||||
|
writeLines([`Downloading to key/${config.social["pgp-key"]}...`, "<br>"]);
|
||||||
|
setTimeout(() => {
|
||||||
|
window.open(
|
||||||
|
`https://src.panahifar.ir/pgp/C86437BFC43196C6.asc`,
|
||||||
|
"_blank"
|
||||||
|
);
|
||||||
|
}, 500);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "linkedin":
|
||||||
|
const linkedinUrl = `https://linkedin.com/in/${config.social.linkedin}`;
|
||||||
|
writeLines([
|
||||||
|
`Redirecting to Linkedin/${config.social.linkedin}...`,
|
||||||
|
"<br>",
|
||||||
|
]);
|
||||||
|
setTimeout(() => {
|
||||||
|
window.open(linkedinUrl, "_blank");
|
||||||
|
}, 500);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "youtube":
|
||||||
|
const youtubeUrl = `https://www.youtube.com/@${config.social.youtube}`;
|
||||||
|
writeLines([
|
||||||
|
`Redirecting to Linkedin/${config.social.linkedin}...`,
|
||||||
|
"<br>",
|
||||||
|
]);
|
||||||
|
setTimeout(() => {
|
||||||
|
window.open(youtubeUrl, "_blank");
|
||||||
|
}, 500);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "nagini":
|
||||||
|
writeLines([`Redirecting to negini/${config.social.git}...`, "<br>"]);
|
||||||
|
setTimeout(() => {
|
||||||
|
window.open(
|
||||||
|
`https://src.panahifar.ir/${config.social.git}`,
|
||||||
|
"_blank"
|
||||||
|
);
|
||||||
|
}, 500);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "email":
|
||||||
|
writeLines([`Redirecting to ${config.social.email}...`, "<br>"]);
|
||||||
|
setTimeout(() => {
|
||||||
|
window.open(`mailto:${config.social.email}`, "_blank");
|
||||||
|
}, 500);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "rm -rf":
|
||||||
|
if (bareMode) {
|
||||||
|
writeLines(["don't try again.", "<br>"]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (isRoot) {
|
||||||
|
writeLines([
|
||||||
|
"Usage: <span class='command'>'rm -rf <dir>'</span>",
|
||||||
|
"<br>",
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
writeLines(["Permission not granted.", "<br>"]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "su":
|
||||||
|
if (bareMode) {
|
||||||
|
writeLines(["no.", "<br>"]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!PASSWORD) return;
|
||||||
|
isPasswordInput = true;
|
||||||
|
USERINPUT.disabled = true;
|
||||||
|
|
||||||
|
if (INPUT_HIDDEN) INPUT_HIDDEN.style.display = "none";
|
||||||
|
PASSWORD.style.display = "block";
|
||||||
|
setTimeout(() => {
|
||||||
|
PASSWORD_INPUT.focus();
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
break;
|
||||||
|
case "ls":
|
||||||
|
if (bareMode) {
|
||||||
|
writeLines(["there's nothing to see here.", "<br>"]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRoot) {
|
||||||
|
writeLines(["/", "<br>"]);
|
||||||
|
} else {
|
||||||
|
writeLines(["Permission not granted.", "<br>"]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (bareMode) {
|
||||||
|
writeLines(["type 'help'", "<br>"]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
writeLines(DEFAULT);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeLines(message: string[]) {
|
||||||
|
message.forEach((item, idx) => {
|
||||||
|
displayText(item, idx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayText(item: string, idx: number) {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!mutWriteLines) return;
|
||||||
|
const p = document.createElement("p");
|
||||||
|
p.innerHTML = item;
|
||||||
|
mutWriteLines.parentNode!.insertBefore(p, mutWriteLines);
|
||||||
|
scrollToBottom();
|
||||||
|
}, 40 * idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
function revertPasswordChanges() {
|
||||||
|
if (!INPUT_HIDDEN || !PASSWORD) return;
|
||||||
|
PASSWORD_INPUT.value = "";
|
||||||
|
USERINPUT.disabled = false;
|
||||||
|
INPUT_HIDDEN.style.display = "block";
|
||||||
|
PASSWORD.style.display = "none";
|
||||||
|
isPasswordInput = false;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
USERINPUT.focus();
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
function passwordHandler() {
|
||||||
|
if (passwordCounter === 2) {
|
||||||
|
if (!INPUT_HIDDEN || !mutWriteLines || !PASSWORD) return;
|
||||||
|
writeLines([
|
||||||
|
"<br>",
|
||||||
|
"INCORRECT PASSWORD.",
|
||||||
|
"PERMISSION NOT GRANTED.",
|
||||||
|
"<br>",
|
||||||
|
]);
|
||||||
|
revertPasswordChanges();
|
||||||
|
passwordCounter = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PASSWORD_INPUT.value === ROOT_PASSWORD) {
|
||||||
|
if (!mutWriteLines || !mutWriteLines.parentNode) return;
|
||||||
|
writeLines([
|
||||||
|
"<br>",
|
||||||
|
"PERMISSION GRANTED.",
|
||||||
|
"Try <span class='command'>'rm -rf'</span>",
|
||||||
|
"<br>",
|
||||||
|
]);
|
||||||
|
revertPasswordChanges();
|
||||||
|
isRoot = true;
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
PASSWORD_INPUT.value = "";
|
||||||
|
passwordCounter++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function easterEggStyles() {
|
||||||
|
const bars = document.getElementById("bars");
|
||||||
|
const body = document.body;
|
||||||
|
const main = document.getElementById("main");
|
||||||
|
const span = document.getElementsByTagName("span");
|
||||||
|
|
||||||
|
if (!bars) return;
|
||||||
|
bars.innerHTML = "";
|
||||||
|
bars.remove();
|
||||||
|
|
||||||
|
if (main) main.style.border = "none";
|
||||||
|
|
||||||
|
body.style.backgroundColor = "black";
|
||||||
|
body.style.fontFamily = "VT323, monospace";
|
||||||
|
body.style.fontSize = "20px";
|
||||||
|
body.style.color = "white";
|
||||||
|
|
||||||
|
for (let i = 0; i < span.length; i++) {
|
||||||
|
span[i].style.color = "white";
|
||||||
|
}
|
||||||
|
|
||||||
|
USERINPUT.style.backgroundColor = "black";
|
||||||
|
USERINPUT.style.color = "white";
|
||||||
|
USERINPUT.style.fontFamily = "VT323, monospace";
|
||||||
|
USERINPUT.style.fontSize = "20px";
|
||||||
|
if (PROMPT) PROMPT.style.color = "white";
|
||||||
|
}
|
||||||
|
|
||||||
|
const initEventListeners = () => {
|
||||||
|
if (HOST) {
|
||||||
|
HOST.innerText = command.hostname;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (USER) {
|
||||||
|
USER.innerText = command.username;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PRE_HOST) {
|
||||||
|
PRE_HOST.innerText = command.hostname;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PRE_USER) {
|
||||||
|
PRE_USER.innerText = command.username;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("load", () => {
|
||||||
|
writeLines(BANNER);
|
||||||
|
});
|
||||||
|
|
||||||
|
USERINPUT.addEventListener("keypress", userInputHandler);
|
||||||
|
USERINPUT.addEventListener("keydown", userInputHandler);
|
||||||
|
PASSWORD_INPUT.addEventListener("keypress", userInputHandler);
|
||||||
|
|
||||||
|
window.addEventListener("click", () => {
|
||||||
|
USERINPUT.focus();
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`%cPassword: ${command.password}`,
|
||||||
|
"color: red; font-size: 20px;"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
initEventListeners();
|
||||||
Reference in New Issue
Block a user