'use strict'; const Net = require('net'); const EventEmitter = require('events'); const Util = require('./util'); const internals = { // Interpret as Command Sequences. kEscape: Buffer.from([0xFF, 0xF4, 0XFF, 0xFD, 0x06]), // IAC IP IAC DO TIMING_MARK kNAWSRequest: Buffer.from([0xFF, 0xFD, 0X1F]), // IAC DO NAWS kNAWSResponse: Buffer.from([0xFF, 0xFB, 0X1F, 0xFF, 0xFA, 0X1F]) // IAC WILL NAWS IAC SB NAWS }; /** * A function that represents a screen, it is called frequently and should * return a string consisting of commands to run. * * @interface IScreen * @type function * @param {Number} modulation A number between 0 and 255 representing the current * step of the modulation * @param {Number} width The width of the screen * @param {Number} height The height of the screen * @param {IRenderer} renderer The renderer used to colorize the scfeen * @return {String} The commands used to render the screen elements */ /** * A function that represents a renderer, it should take in a color in RGB and * return a string with the appropriate code to colorize the terminal. * * @interface IRenderer * @type function * @param {Number} red The red component of the color between 0 and 255 * @param {Number} green The green component of the color between 0 and 255 * @param {Number} blue The green component of the color between 0 and 255 * @return {String} The commands used to colorize the terminal */ /** * The main application for tomato sauce. Listens for connections and serves * random combinations of screens and renderers * * The main entry point is the `#run()` function. * * It emits a listening event that contains the server information on * the `server` key inside the `data` property of the event. * * It also emits an error event that contains the error information on * the `error` key inside the `data` property of the event. * * @class TomatoSauce * @extends EventEmitter * * @param {object} config the configuration object used to extend the properties. * * @property {Array} screens an array of screens available to serve * @property {Array} renderers an array of renderers available to colorize * @property {Number} [port=9999] the port to listen on * @property {Number} [frequency=333] how often to update the screen * @property {Number} [modulation=5] number between 0-255 depicting current modulation step */ const TomatoSauce = class TomatoSauce extends EventEmitter { constructor(config = {}) { super(); this.screens = []; this.renderers = []; // Defaults. this.port = 9999; this.frequency = 333; this.modulation = 5; Object.assign(this, config); } /** * Main entry point, initializes the server and binds events for connections * * @function run * @instance * @memberof TomatoSauce */ run() { this._startServer(); this._bindEvents(); } // Creates a socket, server based on the configuration. Emits the // listening event when ready. _startServer() { const server = Net.createServer(); this.server = server; server.listen(this.port, () => { this.emit('listening', { data: { server } }); }); } // Binds the connection and error events _bindEvents() { // Send the error event all the way up. this.server.on('error', (error) => { this.emit('error', { data: { error } }); }); // Send the error event all the way up. this.server.on('connection', (socket) => { this._renderScreen(socket); }); } // Obtains viewport size, and initializes a random screen with a random // renderer, setting the required interval to draw. _renderScreen(socket) { const connectionData = { width: null, height: null, modulation: 0, screen: this._getScreen(), renderer: this._getRenderer(), socket }; let interval = null; socket.write(internals.kNAWSRequest); socket.on('data', (data) => { if (data.slice(0, 6).compare(internals.kNAWSResponse) === 0) { connectionData.width = Util.parse16BitBuffer(data.slice(6, 8)); connectionData.height = Util.parse16BitBuffer(data.slice(8, 10)); socket.write('\x1B[2J'); // Clear the Screen (CSI 2 J) interval = setInterval(this._writeMessage.bind(this, connectionData), this.frequency); } if (data.compare(internals.kEscape) === 0) { socket.write('\n'); clearInterval(interval); socket.end(); } }); } // Resets the cursor, gets a frame and sends it to the socket. _writeMessage(connectionData) { const payload = connectionData.screen(connectionData.modulation, connectionData.width, connectionData.height, connectionData.renderer); const message = `\x1B[1;1H${payload}`; // reset cursor position before payload connectionData.modulation = (connectionData.modulation + this.modulation) % 256; connectionData.socket.write(message); } _getScreen() { return Util.pickRandom(this.screens); } _getRenderer() { return Util.pickRandom(this.renderers); } }; module.exports = TomatoSauce;