/** * @typedef Command * @property {string} command * @property {string} responseType * @property {string?} value * @property {string[]?} headers * @property {string[]?} rows */ /** * @type {Command[]} commands */ import commands from "./resources/commands.json"; import { getCV, pif, rmRf, setDarkMode } from "./custom-comands"; import { dragElement } from "./draggable"; // Table containing the orders (useful for the completion of the orders) let commandsList = []; commands.forEach((c) => { commandsList.push(c.command); }); // Commands that require JS processing const customCommands = ["clear", "dark", "light", "get cv"]; commandsList = commandsList.concat(customCommands); // Eyster eggs' commands not available for autocompletion const hiddenCommands = ["pif", "rm -rf /"]; // Added the ability to move the window for PCs if (window.innerWidth > 1024) { dragElement(document.querySelector(".terminal")); } // Order history table const commandsHistory = []; let historyMode = false; let historyIndex = -1; const terminalBody = document.querySelector(".terminal__body"); // Adding the default line addNewLine(); // December Easter egg, adding snowflakes const now = new Date(); if (now.getMonth() === 11) { let htmlFlakes = ""; for (let i = 0; i < 6; i++) { htmlFlakes += `
`; } const html = ``; document.body.append(stringToDom(html)); } // Set to dark mode if the browser theme is dark if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { setDarkMode(true); } /** * Returns the HTML of the response for a given command * @param {string} command */ function getDomForCommand(command) { const commandObj = commands.find((el) => el.command === command); let html = ""; if (commandObj === undefined) { html = `'${ command.split(" ")[0] }' is not recognized as an internal command or external command, operable program or batch file. Type the help command to display a list of available commands.`; } else { if (commandObj.responseType === "list" && Array.isArray(commandObj.value)) { html = ""; } else if (commandObj.responseType === "text") { html = commandObj.value; } else if (commandObj.responseType === "table") { const headers = commandObj.headers; const rows = commandObj.rows; const thsHtml = headers.map((h) => `${h}`).join(""); const tdsHtml = rows .map((r) => `${r.map((rtd) => `${rtd}`).join("")}`) .join(""); html = `${thsHtml}${tdsHtml}
`; } else if (commandObj.responseType === "code") { html = `
${commandObj.value.join("\n")}
`; } } return html; } /** * Adds a new command input line and disables the previous one. * @param {string|null} previousUid uid de la ligne précédente. */ function addNewLine(previousUid = null) { const uid = Math.random().toString(36).replace("0.", ""); // terminal__line const terminalLineEl = document.createElement("div"); terminalLineEl.classList.add("terminal__line"); // terminal__response const terminalResponseEl = document.createElement("div"); terminalResponseEl.classList.add("terminal__response"); terminalResponseEl.id = `response-${uid}`; // input text const inputEl = document.createElement("input"); inputEl.type = "text"; inputEl.id = `input-${uid}`; inputEl.autocapitalize = "off"; inputEl.dataset.uid = uid; inputEl.dataset.active = "1"; // Needed for focus inputEl.addEventListener("keydown", onCommandInput); terminalLineEl.appendChild(inputEl); if (previousUid) { const previousInputEl = document.getElementById(previousUid); if (previousInputEl) { previousInputEl.setAttribute("disabled", "true"); previousInputEl.removeEventListener("keydown", onCommandInput); delete previousInputEl.dataset.active; } } document.getElementById("terminal").appendChild(terminalLineEl); document.getElementById("terminal").appendChild(terminalResponseEl); inputEl.focus(); // Adds the focus as soon as the field is created } /** * Manages the keydown on the command input. * @param e */ function onCommandInput(e) { const commandValue = e.target.value.trim().toLowerCase(); if (e.keyCode === 13) { // ENTER if (commandValue !== "") { historyMode = false; const idResponse = `response-${e.target.dataset.uid}`; const responseEl = document.getElementById(idResponse); let html; if ( hiddenCommands.includes(commandValue) || customCommands.includes(commandValue) ) { html = handleCustomCommands(commandValue); } else { html = getDomForCommand(commandValue); } if (responseEl) { responseEl.innerHTML = html; commandsHistory.push(commandValue); addNewLine(e.target.id); } } } else if (e.keyCode === 9) { // TAB e.preventDefault(); if (commandValue === "") { this.value = "help"; } else { const matchingCommand = commandsList.find((c) => c.startsWith(commandValue) ); if (matchingCommand) { this.value = matchingCommand; } } historyMode = false; } else if (e.keyCode === 38 || e.keyCode === 40) { // UP / DOWN // History management if (commandsHistory.length > 0) { if (historyMode === false) { historyIndex = commandsHistory.length - 1; } else { if (e.keyCode === 38 && historyIndex !== 0) { // UP historyIndex--; } else if ( e.keyCode === 40 && historyIndex !== commandsHistory.length - 1 ) { historyIndex++; } } this.value = commandsHistory[historyIndex]; } historyMode = true; } } /** * Allows to manage hidden commands (not proposed in the autocompletion) * @param {string} command * @returns {string|void} Html to be displayed in the response of the command */ function handleCustomCommands(command) { switch (command) { case "pif": pif(); return "Let's go !"; case "light": if (!document.body.classList.contains("dark-mode")) return "You are already in light mode."; setDarkMode(false); return "Your are now in light mode."; case "dark": if (document.body.classList.contains("dark-mode")) return "You are already in dark mode."; setDarkMode(true); return "You are now in dark mode."; case "get cv": getCV(); return "The CV will be downloaded."; case "rm -rf /": rmRf(); return "🎆"; case "clear": terminalBody.innerHTML = `
`; return; } } /** * Convert HTML to DOM object * @param html * @returns {DocumentFragment} */ function stringToDom(html) { return document.createRange().createContextualFragment(html); } // ------------------------------------------------------------------------------------ // EVENT LISTENNER // ------------------------------------------------------------------------------------ // Added focus on the input even if you click on the body (to keep the cursor) document.body.addEventListener("click", function (e) { if (e.target.tagName !== "INPUT") { const activeInput = document.querySelector("input[data-active]"); activeInput.focus(); } }); document.querySelector(".fake-close").addEventListener("click", function (e) { const terminalEl = document.querySelector(".terminal"); terminalEl.parentElement.removeChild(terminalEl); });