feat: implement terminal engine with command dispatch and input handling

This commit is contained in:
ahp
2026-08-25 21:07:15 +03:30
parent 6da5d48fbf
commit dd4feff6a4
+451
View File
@@ -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 &lt;dir&gt;'</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();