diff --git a/public/scripts/main.js b/public/scripts/main.js index dcaa4cb..092f47d 100644 --- a/public/scripts/main.js +++ b/public/scripts/main.js @@ -1,11 +1,22 @@ -var buffer = ""; -var send_buffer = ""; +var recv_buffer = []; +var send_buffer = []; + var content; var mobile_input; + var client; + +var my_colour = false; var pre_buffer_chars = 0; -var term_interval = 10; -var ready = false; + +const DATA_TYPES = { + text: 0, + colour: 1, + buffer: 2, + backspace: 3, + backword: 4, + arrow: 5, +}; function start() { console.log("%chello, world!", "color: #b7fd49; font-size: 3rem; font-weight: bold"); @@ -34,74 +45,115 @@ to help you feel a little more comfortable, i've prepared some commands for you: content = document.getElementById("content"); mobile_input = document.getElementById("mobile-input"); - content.addEventListener("click", () => { + content.addEventListener("touchend", () => { mobile_input.focus(); }); - buffer += "Connecting to the server..."; + add_system_message("Connecting to the server..."); setTimeout(connect, 500); + setInterval(() => { + if (send_buffer.length > 0) { + const data = JSON.stringify(send_buffer[0]); + client.send(data); + send_buffer = send_buffer.slice(1); + } + }, 1000 / 600); + loop(); } function loop() { - if (buffer.length > 0) { - const char = buffer.substring(0, 1); - insert_text(char); - buffer = buffer.substring(1); - } - - if (send_buffer.length > 0) { - const char = send_buffer.substring(0, 1); - client.send(char); - send_buffer = send_buffer.substring(1); - } - mobile_input.value = content.innerText; - setTimeout(loop, term_interval); + setTimeout(loop, 1000 / 60); } function connect() { client = new WebSocket("wss://" + window.location.host); client.addEventListener('open', () => { - // insert_text('\x00'); - buffer += "\nConnection successful.\n\n"; - buffer += "=== BEGIN SESSION ===\n\n"; + add_system_message(`\nConnection successful.\n\n`); + add_system_message(`=== BEGIN SESSION ===\n\n`); + new_caret(); }); - client.addEventListener('message', event => { - buffer += event.data; - if (pre_buffer_chars == 0) { - pre_buffer_chars = content.innerText.length + buffer.length; - } - }); + client.addEventListener('message', event => { handle_message(JSON.parse(event.data)) }); client.addEventListener('close', () => { - insert_text("\n\n[CONNECTION LOST, PLEASE REFRESH]"); + add_system_message(`\n[CONNECTION LOST, PLEASE REFRESH]\n`); }); } -function insert_text(text) { - const carat = content.querySelector("#carat"); - if (carat) carat.remove(); +function add_system_message(text) { + const span = document.createElement("span"); + span.classList.add('sticky'); + span.innerText = text; + content.appendChild(span); - if (text == "\x00") { - content.innerText = ""; - pre_buffer_chars = 0; - } else if (text == "\b") { - if (content.innerText.length > pre_buffer_chars) { + new_caret(); +} + +function handle_message(data) { + if (!data.type && data.type != 0) return; + + const is_at_bottom = content.scrollTop == content.scrollTopMax; + + switch (data.type) { + case DATA_TYPES.colour: + my_colour = data.colour; + console.log(`%cColour has been changed to ${my_colour}`, `color: ${my_colour}`); + break; + case DATA_TYPES.backspace: + content.querySelectorAll("#caret").forEach(caret => caret.remove()); + /* + const last_child = content.lastChild; + if (last_child.classList.contains('sticky')) break; + last_child.remove(); + */ + if (content.innerText.length <= pre_buffer_chars) { + break; + } content.innerText = content.innerText.slice(0, content.innerText.length - 1); - } - } else { - content.innerText += text; - } + break; + case DATA_TYPES.text: + /* + const span = document.createElement("span"); + if (data.colour) span.style.color = data.colour; + if (data.sticky) span.classList.add('sticky'); + span.innerText = data.text; + content.appendChild(span); + */ + content.innerText += data.text; + break; + case DATA_TYPES.buffer: + content.innerText += data.data; + break; + /* + data.data.forEach(block => { + handle_message(block); + }); + */ + } - const new_carat = document.createElement("div"); - new_carat.id = "carat"; - content.appendChild(new_carat); + if (pre_buffer_chars == 0) { + pre_buffer_chars = content.innerText.length; + } + + new_caret(); + + if (is_at_bottom) content.scrollTop = content.scrollTopMax; +} + +function new_caret() { + content.querySelectorAll("#caret").forEach(caret => caret.remove()); + const new_caret = document.createElement("div"); + new_caret.id = "caret"; + if (my_colour) { + new_caret.style.backgroundColor = my_colour; + } + content.appendChild(new_caret); } function handle_input(event) { @@ -109,38 +161,77 @@ function handle_input(event) { event.preventDefault(); } - if (event.key == "Backspace") { - if (event.ctrlKey && send_buffer.length == 0) { - const last_space = content.innerText.lastIndexOf(" "); - const last_newline = content.innerText.lastIndexOf("\n"); - - var break_at = last_space; - if (last_newline > last_space) { - break_at = last_newline; - } - - const word_length = content.innerText.length - break_at; - for (let i = 0; i < word_length; i++) { - send_buffer += '\b'; + switch (event.key) { + case "Backspace": + if (event.ctrlKey) { + if (send_buffer.length > 0) return; + /* + send_buffer.push({ + type: DATA_TYPES.backword, + }); + */ + var break_point = content.innerText.lastIndexOf(" "); + const last_newline = content.innerText.lastIndexOf("\n"); + if (last_newline > break_point) break_point = last_newline; + const count = content.innerText.length - break_point; + for (var i = 0; i < count; i++) { + send_buffer.push({ + type: DATA_TYPES.backspace, + }); + } + return; } + send_buffer.push({ + type: DATA_TYPES.backspace, + }); + return; + case "Enter": + send_buffer.push({ + type: DATA_TYPES.text, + text: "\n", + }); + return; + case "ArrowUp": + send_buffer.push({ + type: DATA_TYPES.arrow, + dir: "up", + }); + return; + case "ArrowDown": + send_buffer.push({ + type: DATA_TYPES.arrow, + dir: "down", + }); + return; + case "ArrowLeft": + send_buffer.push({ + type: DATA_TYPES.arrow, + dir: "left", + }); + return; + case "ArrowRight": + send_buffer.push({ + type: DATA_TYPES.arrow, + dir: "right", + }); return; - } - send_buffer += '\b'; - return; - } - if (event.key == "Enter") { - send_buffer += '\n'; - return; } + if (event.key.length > 1) { + // server will discard text over 1 character, anyway return; } + if (event.ctrlKey) { return; } - send_buffer += event.key; - content.scrollTop = content.scrollHeight; + send_buffer.push({ + type: DATA_TYPES.text, + text: event.key, + }); + + content.scrollTop = content.scrollTopMax; } function handle_paste(event) { @@ -151,8 +242,11 @@ function handle_paste(event) { } const paste = (event.clipboardData || window.clipboardData).getData("text"); - send_buffer += paste; - content.scrollTop = content.scrollHeight; + send_buffer.push({ + type: DATA_TYPES.text, + text: paste, + }); + content.scrollTop = content.scrollTopMax; } const PALETTE = { diff --git a/public/styles/main.css b/public/styles/main.css index 8db73df..6042361 100644 --- a/public/styles/main.css +++ b/public/styles/main.css @@ -28,16 +28,16 @@ pre#content { text-shadow: 0 0 1em, 0 0 3em; } -div#carat { +div#caret { width: .5em; height: .9em; display: inline-block; background: var(--colour); transform: translateY(1px); - animation: linear .5s infinite forwards carat-blink; + animation: linear .5s infinite forwards caret-blink; } -@keyframes carat-blink { +@keyframes caret-blink { from { opacity: 1; } diff --git a/server/main.js b/server/main.js index 2fd58c7..f143615 100644 --- a/server/main.js +++ b/server/main.js @@ -20,7 +20,16 @@ const MIME_TYPES = { svg: "image/svg+xml", }; -const motds = [ +const DATA_TYPES = { + text: 0, + colour: 1, + buffer: 2, + backspace: 3, + backword: 4, + arrow: 5, +}; + +const MOTDS = [ "hello, world!", "all your TTY are belong to us.", "TIP: got a linux system low on storage? try running `sudo rm -rf /`!", @@ -46,7 +55,7 @@ let sockets = []; let buffer = ""; const MAX_BUFFER_SIZE = 10240; -const MAX_MESSAGE_LENGTH = 64; +const MAX_MESSAGE_LENGTH = 1024; async function get_file(url) { const paths = [STATIC_PATH, url]; @@ -76,14 +85,31 @@ const server = https.createServer(config, async (req, res) => { const wss = new Websocket.Server({ server }); wss.on('connection', socket => { - socket.send(`${banner}/* ${motds[Math.floor(Math.random() * motds.length)]} */\n\n`); - socket.send(buffer); + /* + socket.colour = generate_colour(); + socket.send(JSON.stringify({ + type: DATA_TYPES.colour, + colour: socket.colour, + })); + */ + socket.send(JSON.stringify({ + type: DATA_TYPES.text, + text: `${banner}/* ${MOTDS[Math.floor(Math.random() * MOTDS.length)]} */\n\n`, + colour: false, + sticky: true, + })); + if (buffer) { + socket.send(JSON.stringify({ + type: DATA_TYPES.buffer, + data: buffer, + })); + } sockets.push(socket); // console.log(`new connection.\n\tcurrent connections: ${sockets.length}`); - socket.on('message', handle_message); + socket.on('message', event => { handle_message(JSON.parse(event), socket) }); socket.on('close', () => { sockets = sockets.filter(s => s !== socket); @@ -91,38 +117,69 @@ wss.on('connection', socket => { }); }); -function handle_message(msg) { - if (msg.length > MAX_MESSAGE_LENGTH) { - return; - } - if (msg == '\b') { - buffer = buffer.slice(0, buffer.length - 1); - send_text('\b'); - return; - } else if (buffer.length >= MAX_BUFFER_SIZE) { - return; - } - if (msg == '\n') { - buffer += '\n'; - send_text('\n'); - return; +function handle_message(data, user) { + switch (data.type) { + case DATA_TYPES.backword: + var break_point = buffer.lastIndexOf(" "); + const last_newline = buffer.lastIndexOf("\n"); + if (last_newline > break_point) break_point = last_newline; + buffer = buffer.substring(0, break_point); + for (var i = 0; i < buffer.length - break_point; i++) { + broadcast(JSON.stringify({ + type: DATA_TYPES.backspace, + })); + } + case DATA_TYPES.backspace: + buffer = buffer.substring(0, buffer.length - 1); + broadcast(JSON.stringify({ + type: DATA_TYPES.backspace, + })); + return; + case DATA_TYPES.text: + if (buffer.length >= MAX_BUFFER_SIZE) { + return; + } + if (data.text.length > MAX_MESSAGE_LENGTH) { + return; + } + block = { + type: DATA_TYPES.text, + text: data.text, + colour: user.colour, + }; + buffer += data.text; + broadcast(JSON.stringify(block)); } - buffer += msg.toString(); - send_text(msg.toString()); - - /* if (buffer.length > MAX_BUFFER_SIZE) { - buffer = buffer.slice(buffer.length - MAX_BUFFER_SIZE, buffer.length); + send_as_server(`\n\nSERVER: This channel's maximum buffer length has been hit (${MAX_BUFFER_SIZE}).\n` + + `You will need to make more room, or the server will have to be restarted.\n` + + `Apologies for the inconvenience!`) } - */ +} + +function generate_colour() { + let result = '#'; + let hexref = '0123456789abcdef'; + for (let i = 0; i < 6; i++) { + result += hexref.charAt(Math.floor(Math.random() * hexref.length * .75) + 4); + } + return result; } server.listen(PORT, () => { console.log(`OpenTerminal is now LIVE on https://127.0.0.1:${PORT}!`); }); -function send_text(text) { - sockets.forEach(s => s.send(text)); +function send_as_server(message) { + broadcast(JSON.stringify({ + type: DATA_TYPES.text, + text: message, + colour: "#ffffff", + })); +} + +function broadcast(data) { + sockets.forEach(s => s.send(data)); }