UNPKG

hertzscript-velocity

Version:

Velocity is a concurrent REPL which is designed specifically for HertzScript coroutines. All code runs concurrently alongside the REPL, so you can continue typing/adding code while existing code is still running.

322 lines (321 loc) 10.7 kB
#!/usr/bin/env node // Concurrent Hertzscript Velocity REPL function generateHzModule(hzSource) { // Compile the source code into a HertzScript Module return eval("(hzUserLib) => { return " + hzSource + "};"); } (function () { const CaptureConsole = require("@aoberoi/capture-console").CaptureConsole; const Dispatcher = require("hertzscript-dispatcher"); const hzCompile = require("hertzscript-compiler"); const term = require("terminal-kit"); const fs = require("fs"); const os = require("os"); // Runs all compiled source code concurrently in the event loop var hzDisp = new Dispatcher(); // Captures all console output from code inside the dispatcher const cConsole = new CaptureConsole(); // ANSI Escape Codes const ansiRegexp = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g; // Input history file size limit if ("HZREPL_HISTORY_SIZE" in process.env) var historyLimit = Number(process.env.HZREPL_HISTORY_SIZE); else var historyLimit = 1000; // Input history file path if ("HZREPL_HISTORY" in process.env) var historyPath = String(process.env.HZREPL_HISTORY); else var historyPath = os.homedir() + "/.hertzscript_repl_history"; // Enables input history reading and writing const writeHistory = historyPath !== ""; // Import previous session(s) input history var prevHistory = []; if (writeHistory) { if (!fs.existsSync(historyPath)) fs.closeSync(fs.openSync(historyPath, 'w')); const history = fs.readFileSync(historyPath).toString(); if (history.length > 0) prevHistory = history.split("\n"); } var inputHistory = [].concat(prevHistory); const pipes = { vertLine: "│", horizLine: "─", cross: "┼", topRight: "┐", topLeft: "┌", botRight: "┘", botLeft: "└", topT: "┬", botT: "┴", leftT: "├", rightT: "┤" }; // Draws a box using pipe characters function drawBox(height, width, x = 0, y = 0) { term.terminal.moveTo(x, y); const leftSpaces = new Array(y).fill(" ").join(""); const horizLine = new Array(width - 2).fill(pipes.horizLine).join(""); const top = pipes.topLeft + horizLine + pipes.topRight; const bot = pipes.botLeft + horizLine + pipes.botRight; const middleSpaces = new Array(top.length - 2).fill(" ").join(""); const middleRow = leftSpaces + pipes.vertLine + middleSpaces + pipes.vertLine + "\n"; const middle = new Array(height - 1).fill(middleRow).join(""); return "\n" + leftSpaces + top + "\n" + middle + leftSpaces + bot + "\n"; } // Screen buffer for the status bar const statScreenBuffer = new term.ScreenBuffer({ dst: term.terminal, width: process.stdout.columns, height: 1, x: 1, y: 1 }); // Text buffer for the status bar const statTextBuffer = new term.TextBuffer({ dst: statScreenBuffer, width: process.stdout.columns, height: 1, x: 0, y: 0 }); // Contains all text within the REPL output window var textLines = []; // The index to start drawing "textLines" from var startIndex = 0; // Draws the status bar across the top of the terminal window function drawStats() { term.terminal.saveCursor(); if (startIndex === 0) var scrollText = "Scroll: 0% "; else var scrollText = "Scroll: " + ((startIndex / textLines.length) * 100).toFixed() + "% "; var linesText = " Lines: " + textLines.length; var titleText = "HzVelocity REPL | " + hzDisp.queue.blocks.length + " Coroutines " + (paused ? "Paused" : "Running"); var spacesLen = Number(((process.stdout.columns - (scrollText.length + linesText.length + titleText.length)) / 2).toFixed()); var spaces = (new Array(spacesLen)).fill(" ").join(""); var text = linesText + spaces + titleText + spaces + scrollText; statTextBuffer.setText(text); statTextBuffer.setAttrRegion({ bgColor: "white", color: "black" }, { xmin: 0, xmax: process.stdout.columns, ymin: 0, ymax: 1 }); statTextBuffer.draw(); statScreenBuffer.draw(); term.terminal.restoreCursor(); } // Draw the REPL output window process.stdout.write(drawBox(process.stdout.rows - 2, process.stdout.columns)); // Inserts newlines to wrap text at a specific width function wordWrap(str, width) { if (str.length <= width) return str; var count = 0; var newStr = []; for (const char of str) { count++; if (char === "\n") { newStr.push(char); count = 0; } else if (count >= width) { newStr.push(char + "\n"); count = 0; } else { newStr.push(char); } } return newStr.join(""); } // Screen buffer for the REPL output window const logScreenBuffer = new term.ScreenBuffer({ dst: term.terminal, width: process.stdout.columns - 6, height: process.stdout.rows - 3, x: 3, y: 2, //delta: true }); // Text buffer for the REPL output window const logTextBuffer = new term.TextBuffer({ dst: logScreenBuffer, width: process.stdout.columns - 6, height: process.stdout.rows - 3, x: 0, y: 0, wrap: true }); // Draws text in the REPL output window function logDraw() { term.terminal.saveCursor(); logTextBuffer.draw(); logScreenBuffer.draw(); term.terminal.restoreCursor(); } // Adds text to the REPL output window function logText(str) { if (str.length === 0) return; const strings = wordWrap(str, process.stdout.columns - 6).split("\n"); for (const string of strings) if (string !== "") textLines.push(string); startIndex = textLines.length - (process.stdout.rows - 4); if (startIndex < 0) startIndex = 0; logTextBuffer.setText(textLines.slice(startIndex).join("\n")); logDraw(); } // Scrolls the REPL output window up or down function logScroll(offset) { const index = startIndex + offset; if (index > textLines.length || index < 0) return; startIndex = index; logTextBuffer.setText(textLines.slice(startIndex).join("\n")); logDraw(); } // Clears all text from the REPL output window function clearLogText() { textLines = []; startIndex = 0; logTextBuffer.setText(""); logTextBuffer.moveTo(0, 0); logDraw(); } // Writes session input history and terminates the application function exit() { if (writeHistory && inputHistory.length > 0 && inputHistory.length !== prevHistory.length) { if (inputHistory.length > historyLimit && historyLimit > 0) inputHistory = inputHistory.slice((inputHistory.length - 1) - (historyLimit - 1)); if (inputHistory.length > prevHistory.length && prevHistory.length > 0) inputHistory = inputHistory.slice(prevHistory.length); if (!fs.existsSync(historyPath)) fs.closeSync(fs.openSync(historyPath, 'w')); fs.appendFileSync(historyPath, (prevHistory.length > 0 ? "\n" : "") + inputHistory.join("\n")); } process.exit(0); } // Set to true if user sent an exit signal as their last action, // two successive exit signals trigger "exit()". var exiting = false; term.terminal.on("key", (name, data) => { if (name === "CTRL_C") { // Exit application if (exiting) exit(); exiting = true; logText("(To exit, press ^C again or type .exit)\n"); } else if (name === "CTRL_P") { // Pause all dispatcher execution paused ? startExec() : pauseExec(); } else if (name === "CTRL_R") { resetQueue(); } else if (name === "PAGE_DOWN") { // Scrolls the REPL output window down by 1 line logScroll(1); drawStats(); } else if (name === "PAGE_UP") { // Scrolls the REPL output window up by 1 line logScroll(-1); drawStats(); } }); // Draws captured console output to the REPL output window function drawCapture() { const cBuffer = cConsole.getCapturedText(); if (cBuffer.length === 0) return; for (var str of cBuffer) { if (str === "\x1b[0J") clearLogText(); else logText(str.replace(ansiRegexp, "")); drawStats(); } cConsole.clearCaptureText(); } // Write the input line indicator term.terminal.nextLine(2); process.stdout.write("> "); term.terminal.saveCursor(); logText(`Welcome to the concurrent HertzScript Velocity REPL!\n Press PageUp/PageDown to scroll.\n Press CTRL+C twice or type ".exit" to quit.\n Press CTRL+P or type .pause to pause/unpause coroutine execution.`); var paused = false; // Pauses dispatcher execution function pauseExec() { logText("Coroutine execution paused."); paused = true; drawStats(); } // Kills all coroutines function resetQueue() { while (hzDisp.queue.blocks.length > 0) hzDisp.killLast(); logText("All coroutines terminated."); } // Starts dispatcher execution function startExec() { logText("Coroutine execution started.") paused = false; hzDisp.running = true; setTimeout(execRunner, 5); setTimeout(logRunner, 5); drawStats(); } // Draws captured console output to the REPL output window every 5ms const logRunner = () => { drawCapture(); drawStats(); if (!paused) setTimeout(logRunner, 5); }; // Executes the dispatcher for 1ms every 5ms const execRunner = () => { if (paused) return; cConsole.startCapture(); hzDisp.cycle(1); cConsole.stopCapture(); if (hzDisp.running && !paused) setTimeout(execRunner, 5); }; function dotcodeHandler(input) { // Exit application if (input === ".exit") exit(); // Pause/unpause all dispatcher execution else if (input === ".pause") paused ? startExec() : pauseExec(); // Terminates all coroutines else if (input === ".kill") resetQueue(); else return false; return true; } // Handles user input source code from the main input line const inputHandler = (error, input) => { if (error) throw error; logText("> " + input); // Ignore whitespace if (!(/\S/.test(input))) { inputField(); term.terminal.restoreCursor(); return; } term.terminal.eraseLine(); if (input[0] !== "." || !dotcodeHandler(input)) { if (inputHistory[inputHistory.length - 1] !== input) inputHistory.push(input); // Interrupt a prior exit signal exiting = false; process.stdout.write("⌛"); try { // Compile the source code into a HertzScript Module, // import the HzModule into the dispatcher. hzDisp.import(generateHzModule(hzCompile("(function (){" + input + "})", false, false, true))); // Start execution of the dispatcher if it's not paused or running if (!hzDisp.running && !paused) startExec(); } catch (error) { cConsole.startCapture(); console.error(error); cConsole.stopCapture(); drawCapture(); } } term.terminal.eraseLine(); term.terminal.left(input.length + 4); process.stdout.write("> "); drawStats(); inputField(); }; const fieldOptions = { cancelable: true, history: inputHistory, cursorPosition: -1 }; // Readies the user input line for input const inputField = () => { term.terminal.inputField(fieldOptions, inputHandler); }; drawStats(); inputField(); })();