let disableInput = false; let prompt = '\x1b[33mlemma$\x1b[0m '; let commandBuffer = ''; let toollist = []; let lineBuffer = []; let history = []; let historyIndex = -1; let offset = 0; function stripAnsiCodes(str) { // Regular expression to match ANSI escape codes const ansiRegex = /\x1b\[[0-9;]*m/g; return str.replace(ansiRegex, ''); } async function load_tools() { try { const response = await fetch('/tools.json'); if (!response.ok) { throw new Error('Network response was not ok'); } const tools = await response.json(); return tools; } catch (error) { console.error('Error fetching tools:', error); } return []; } function populate_tools() { load_tools().then((tools) => { toollist = []; tools.forEach(tool => { toollist.push(tool); }); }); } function list_tools(terminal) { load_tools().then((tools) => { terminal.write('Available Remote Tools:\r\n'); toollist = []; tools.forEach(tool => { toollist.push(tool); terminal.write(` \u001b[38;2;145;231;255m${tool}\u001b[0m\r\n`); }); terminal.write("\r\n"); terminal.write("Run using \x1b[32mrun \u001b[38;2;145;231;255m \x1b[0m or \x1b[32mfork \u001b[38;2;145;231;255m \x1b[0m or simply \u001b[38;2;145;231;255m \x1b[0m\r\n"); terminal.write(prompt); }); } function typeNextChar(terminal, text, delay) { return new Promise((resolve) => { let index = 0; function step() { if (index < text.length) { terminal.write(text[index]); index++; setTimeout(step, delay); } else { terminal.write('\r\n\r\n'); list_tools(terminal); disableInput = false; resolve(); } } step(); }); } async function printWithDelay(terminal, text, delay) { await typeNextChar(terminal, text, delay); } async function intro(terminal) { disableInput = true; const sstr = "\u001b\u005b\u0033\u0033\u006d\u000d\u000a\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u2588\u2588\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u0020\u0020\u2590\u2588\u2588\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u2584\u2588\u2584\u0020\u0020\u0020\u0020\u0020\u2588\u2588\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u2584\u2588\u2584\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u2584\u2584\u0020\u0020\u0020\u000d\u000a\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u2588\u2588\u258c\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u2580\u2580\u0020\u2590\u2588\u2588\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u2588\u2588\u2588\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u2588\u2588\u2588\u258c\u0020\u0020\u0020\u0020\u0020\u2588\u2588\u2588\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u2588\u2588\u2588\u258c\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u2588\u2588\u2588\u2588\u0020\u0020\u0020\u000d\u000a\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u2590\u2588\u2588\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u2588\u2588\u2588\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u2588\u2588\u2588\u258c\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u2588\u2588\u2588\u2588\u0020\u0020\u0020\u0020\u0020\u0020\u2588\u2588\u2588\u258c\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u2588\u2588\u2588\u2588\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u2584\u2588\u0020\u0020\u2588\u2588\u0020\u0020\u0020\u000d\u000a\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u2588\u2588\u258c\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u2584\u2588\u2588\u2588\u2584\u2584\u2584\u2584\u2584\u0020\u0020\u0020\u0020\u0020\u2590\u2588\u2588\u2588\u2588\u0020\u0020\u0020\u0020\u0020\u0020\u2588\u2588\u2588\u2588\u2588\u0020\u0020\u0020\u0020\u0020\u2590\u2588\u2588\u2588\u2588\u2584\u0020\u0020\u0020\u0020\u0020\u2588\u2588\u2588\u2588\u2588\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u2588\u2580\u0020\u0020\u0020\u2588\u2588\u258c\u0020\u0020\u000d\u000a\u0020\u0020\u0020\u0020\u0020\u0020\u2590\u2588\u2588\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u2590\u2588\u2588\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u2588\u2588\u0020\u2590\u2588\u2588\u0020\u0020\u0020\u2584\u2588\u2580\u0020\u2588\u2588\u2588\u0020\u0020\u0020\u0020\u0020\u2588\u2588\u0020\u0020\u2588\u2588\u2584\u0020\u0020\u2584\u2588\u2580\u0020\u2588\u2588\u2588\u0020\u0020\u0020\u0020\u0020\u0020\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u0020\u0020\u000d\u000a\u0020\u0020\u0020\u0020\u0020\u2588\u2588\u2588\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u2588\u2588\u2588\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u2588\u2588\u2588\u0020\u0020\u0020\u2588\u2588\u2588\u2588\u2580\u0020\u0020\u0020\u2588\u2588\u258c\u0020\u0020\u0020\u0020\u2590\u2588\u2588\u0020\u0020\u0020\u2588\u2588\u2588\u2588\u2580\u0020\u0020\u0020\u2588\u2588\u258c\u0020\u0020\u0020\u0020\u2584\u2588\u2588\u0020\u0020\u0020\u0020\u0020\u0020\u2580\u2588\u2588\u0020\u0020\u000d\u000a\u0020\u0020\u0020\u0020\u0020\u2588\u2588\u258c\u0020\u0020\u0020\u0020\u0020\u0020\u2584\u2584\u0020\u0020\u0020\u2588\u2588\u258c\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u2588\u2588\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u2588\u2588\u258c\u0020\u0020\u0020\u0020\u2588\u2588\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u2588\u2588\u258c\u0020\u0020\u0020\u2584\u2588\u2588\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u2588\u2588\u2588\u0020\u000d\u000a\u0020\u0020\u0020\u0020\u0020\u2580\u2588\u2588\u2588\u2588\u2588\u2588\u2580\u2580\u2580\u0020\u0020\u0020\u0020\u2580\u2588\u2588\u2588\u2588\u2588\u2580\u2580\u2580\u0020\u0020\u0020\u2590\u2588\u2588\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u2588\u2588\u258c\u0020\u0020\u0020\u0020\u2588\u2588\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u2588\u2588\u258c\u0020\u0020\u0020\u2588\u2588\u2580\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020\u2588\u2588\u2580\u001b\u005b\u0030\u006d\u000d\u000a" terminal.write(sstr+ "\r\n\r\n"); terminal.write(' '); const phrase = 'Response Streaming CLI Tools on AWS Lambda'; const delay = 25; // Delay in milliseconds printWithDelay(terminal, phrase, delay); } async function execute_remote_tool(terminal, args) { const abortController = new AbortController(); try { const url = new URL('/runtool', window.location.origin); url.searchParams.set('cmd', encodeURIComponent(args)); url.searchParams.set('verbose', "true"); const response = await fetch(url.toString(), { method: 'POST', signal: abortController.signal, }); if (!response.body) { throw new Error('ReadableStream not supported.'); } // Get the timeout from the header and set the timeout const timeoutHeader = response.headers.get('X-Lemma-Timeout'); const timeout = parseInt(timeoutHeader, 10) * 1000; // Convert to milliseconds // Set a timeout to abort the request const timeoutId = setTimeout(() => { abortController.abort(); terminal.write('\r\n\u001b[31mError: Stream timeout exceeded.\x1b[0m\r\n'); }, timeout); const reader = response.body.getReader(); const decoder = new TextDecoder('utf-8'); while (true) { const { done, value } = await reader.read(); if (done) { clearTimeout(timeoutId); // Clear the timeout if done break; } let chunk = decoder.decode(value, { stream: true }); // replace any \n with \r\n chunk = chunk.replace(/\n/g, '\r\n'); terminal.write(chunk); } terminal.write('\r\n\u001b[38;2;145;231;255mRemote tool execution complete.\x1b[0m\r\n'); disableInput = false; terminal.write(prompt); } catch (error) { if (error.name === 'AbortError') { terminal.write('\r\n\u001b[31mError: Remote execution failed due to timeout.\x1b[0m\r\n'); } else { terminal.write(`\r\nError: ${error.message}\r\n`); } disableInput = false; terminal.write(prompt); } } document.addEventListener('DOMContentLoaded', () => { let truerows = 45; let truecols = 150; if (terminalContainer.clientWidth <= 1024) { truecols = 100; } const terminal = new Terminal({ rows: truerows, cols: truecols }); const fitAddon = new FitAddon.FitAddon(); terminal.loadAddon(fitAddon); function fitTerminal() { const containerWidth = terminalContainer.clientWidth; const containerHeight = terminalContainer.clientHeight; const cols = truecols; const rows = truerows; const cellWidth = containerWidth / cols; const cellHeight = containerHeight / rows; // Set the font size based on the smallest dimension to maintain aspect ratio const fontSize = Math.min(cellWidth, cellHeight) * 1.6; console.log(cols) // // // Apply the calculated font size to the terminal // // Apply the calculated font size to the terminal terminal.options.fontSize = fontSize; fitAddon.fit(); console.log("fitting terminal") } terminal.open(document.getElementById('terminalContainer')); fitTerminal(); terminal.focus(); // Adjust terminal size when window is resized window.addEventListener('resize', fitTerminal); // check if the command query has been set const urlParams = new URLSearchParams(window.location.search); const command = urlParams.get('cmd'); if (command) { populate_tools(); terminal.write(prompt+`${command}\r\n`); executeCommand(command); } else { intro(terminal); } async function simpleShell(term, data) { let CurX = term.buffer.active.cursorX; let CurY = term.buffer.active.cursorY; let MaxX = term.cols; let MaxY = term.rows; if (disableInput === true) { return; } // string splitting is needed to also handle multichar input (eg. from copy) for (let i = 0; i < data.length; ++i) { const c = data[i]; if (c === '\r') { // was pressed case offset = 0; term.write('\r\n'); if (lineBuffer.length) { // we have something in line buffer, normally a shell does its REPL logic here // for simplicity - just join characters and exec... const command = lineBuffer.join(''); lineBuffer.length = 0; history.push(command); historyIndex = history.length; executeCommand(command); } else { term.write(prompt); } } else if (c === '\x7F') { // was pressed case if (lineBuffer.length) { if (offset === 0) { if (CurX === 0) { // go to the previous line end term.write('\x1b[1A'); // control code: move up one line term.write('\x1b[' + MaxX + 'C'); // control code: move to the end of the line } lineBuffer.pop(); term.write('\b \b'); } } } else if (['\x1b[5', '\x1b[6'].includes(data.slice(i, i + 3))) { // not implemented i += 3; } else if (['\x1b[F', '\x1b[H'].includes(data.slice(i, i + 3))) { if (data.slice(i, i + 3) === '\x1b[H') { // Home key // not implemented } else if (data.slice(i, i + 3) === '\x1b[F') { // End key // not implemented } i += 3; } else if (['\x1b[A', '\x1b[B', '\x1b[C', '\x1b[D'].includes(data.slice(i, i + 3))) { // keys pressed if (data.slice(i, i + 3) === '\x1b[A') { // up arrow if (historyIndex > 0) { historyIndex--; updateCommandBuffer(history[historyIndex]); } } else if (data.slice(i, i + 3) === '\x1b[B') { // down arrow if (historyIndex < history.length - 1) { historyIndex++; updateCommandBuffer(history[historyIndex]); } else { historyIndex = history.length; updateCommandBuffer(''); } } else if (data.slice(i, i + 3) === '\x1b[C') { // right arrow // not implemented } else if (data.slice(i, i + 3) === '\x1b[D') { // left arrow // not implemented } i += 2; } else { // push everything else into the line buffer and echo back to user // if we are at the end of the line, // move up a row and to the beginning of the line if (CurX === MaxX - 1) { term.write('\r\n'); } lineBuffer.push(c); term.write(c); } } } terminal.onData(data => simpleShell(terminal, data)); function executeCommandSingle(command) { // Empty function for now //terminal.write(`Executing command: ${command}\r\n`); // split command and get first token const command0 = command.split(' ')[0]; const command1 = command.split(' ')[1]; if (command0 === 'help') { terminal.write('Available Local Commands:\r\n'); terminal.write(' \x1b[32mhelp -\x1b[0m Show this help message\r\n'); terminal.write(' \x1b[32mclear -\x1b[0m Clear the terminal\r\n'); terminal.write(' \x1b[32mtools -\x1b[0m Show a list of remote tools\r\n'); terminal.write(' \x1b[32msize -\x1b[0m Show or Set terminal size (i.e size, size 45x100)\r\n'); terminal.write(' \x1b[32mrun -\x1b[0m Run a remote tool in the current terminal\r\n'); terminal.write(' \x1b[32mfork -\x1b[0m Run a remote tool in a new terminal\r\n'); terminal.write(prompt); } else if (command0 === 'clear') { terminal.clear(); terminal.write(prompt); } else if (command0 === 'tools') { list_tools(terminal); } else if (command0 === 'reset') { truerows = 45; truecols = 150; if (terminalContainer.clientWidth <= 1024) { truecols = 100; } terminal.clear(); terminal.resize(truerows, truecols); fitTerminal() intro(terminal); } else if (command0 === 'size') { if (command1 === undefined) { terminal.write("Terminal size: " + truerows + "x" + truecols + "\r\n"); terminal.write(prompt); return } const r = command1.split('x')[0]; const c = command1.split('x')[1]; if (r === undefined || c === undefined) { terminal.write(prompt); return } // resize the terminal based on r and c terminal.resize(r, c); truerows = r; truecols = c; fitTerminal() terminal.write(prompt); } else if (command0 === 'fork') { const args = ("run " + command.split(' ').slice(1).join(' ')); const url = new URL(window.location.href); const finalurl = url.origin + url.pathname + "?cmd=" + encodeURIComponent(args); window.open(finalurl, '_blank'); terminal.write(prompt); } else if (command0 === 'run') { const args = command.split(' ').slice(1).join(' '); disableInput = true; execute_remote_tool(terminal, args); } else { // check if the command is a tool if (toollist.includes(command0)) { disableInput = true; execute_remote_tool(terminal, command); } else { terminal.write(`\x1b[31mCommand not found:\x1b[0m ${command0}\r\n`); terminal.write(prompt); } } } function executeCommand(command) { if (command.includes(';')) { const commands = command.split(';'); commands.forEach((cmd) => { // Execute each command in the list and trim any leading/trailing whitespace executeCommandSingle(cmd.trim()); }); } else { executeCommandSingle(command); } } function updateCommandBuffer(command) { // Clear current line terminal.write('\r'+prompt + ' '.repeat(lineBuffer.length) + '\r'+prompt); // push every character in the command to the lineBuffer lineBuffer = command.split(''); // Convert the command string into an array of characters let commandArray = command.split(''); let promptlen = stripAnsiCodes(prompt).length let i = promptlen; while (i < commandArray.length) { if (i % terminal.cols === 0 && i !== 0 ) { commandArray.splice(i-promptlen-1, 0, '\r\n'); } i++; } terminal.write(commandArray.join('')); } // Apply custom CSS to make the scrollbar invisible const terminalElement = document.querySelector('#terminal .xterm-viewport'); if (terminalElement) { terminalElement.style.overflowY = 'hidden'; } });