]> git.r.bdr.sh - rbdr/tomato-sauce/blob - lib/tomato_sauce.js
f3017ea8f1789516cd618c618d85cd2b61ecccaf
[rbdr/tomato-sauce] / lib / tomato_sauce.js
1 'use strict';
2
3 const Net = require('net');
4 const EventEmitter = require('events');
5
6 const Util = require('./util');
7
8 const internals = {
9 // Interpret as Command Sequences.
10 kEscape: new Buffer([0xFF, 0xF4, 0XFF, 0xFD, 0x06]), // IAC IP IAC DO TIMING_MARK
11 kNAWSRequest: new Buffer([0xFF, 0xFD, 0X1F]), // IAC DO NAWS
12 kNAWSResponse: new Buffer([0xFF, 0xFB, 0X1F, 0xFF, 0xFA, 0X1F]) // IAC WILL NAWS IAC SB NAWS
13 };
14
15 /**
16 * A function that represents a screen, it is called frequently and should
17 * return a string consisting of commands to run.
18 *
19 * @interface IScreen
20 * @type function
21 * @param {Number} modulation A number between 0 and 255 representing the current
22 * step of the modulation
23 * @param {Number} width The width of the screen
24 * @param {Number} height The height of the screen
25 * @param {IRenderer} renderer The renderer used to colorize the scfeen
26 * @return {String} The commands used to render the screen elements
27 */
28
29 /**
30 * A function that represents a renderer, it should take in a color in RGB and
31 * return a string with the appropriate code to colorize the terminal.
32 *
33 * @interface IRenderer
34 * @type function
35 * @param {Number} red The red component of the color between 0 and 255
36 * @param {Number} green The green component of the color between 0 and 255
37 * @param {Number} blue The green component of the color between 0 and 255
38 * @return {String} The commands used to colorize the terminal
39 */
40
41 /**
42 * The main application for tomato sauce. Listens for connections and serves
43 * random combinations of screens and renderers
44 *
45 * The main entry point is the `#run()` function.
46 *
47 * It emits a listening event that contains the server information on
48 * the `server` key inside the `data` property of the event.
49 *
50 * It also emits an error event that contains the error information on
51 * the `error` key inside the `data` property of the event.
52 *
53 * @class TomatoSauce
54 * @extends EventEmitter
55 *
56 * @param {object} config the configuration object used to extend the properties.
57 *
58 * @property {Array<IScreen>} screens an array of screens available to serve
59 * @property {Array<IRenderer>} renderers an array of renderers available to colorize
60 * @property {Number} [port=9999] the port to listen on
61 * @property {Number} [frequency=333] how often to update the screen
62 * @property {Number} [modulation=5] number between 0-255 depicting current modulation step
63 */
64 const TomatoSauce = class TomatoSauce extends EventEmitter {
65
66 constructor(config = {}) {
67
68 super();
69
70 this.screens = [];
71 this.renderers = [];
72
73 // Defaults.
74 this.port = 9999;
75 this.frequency = 333;
76 this.modulation = 5;
77
78 Object.assign(this, config);
79 }
80
81 /**
82 * Main entry point, initializes the server and binds events for connections
83 *
84 * @function run
85 * @instance
86 * @memberof TomatoSauce
87 */
88 run() {
89
90 this._startServer();
91 this._bindEvents();
92 }
93
94 // Creates a socket, server based on the configuration. Emits the
95 // listening event when ready.
96
97 _startServer() {
98
99 const server = Net.createServer();
100 this.server = server;
101 server.listen(this.port, () => {
102
103 this.emit('listening', {
104 data: {
105 server
106 }
107 });
108 });
109 }
110
111 // Binds the connection and error events
112
113 _bindEvents() {
114
115 // Send the error event all the way up.
116 this.server.on('error', (error) => {
117
118 this.emit('error', {
119 data: {
120 error
121 }
122 });
123 });
124
125 // Send the error event all the way up.
126 this.server.on('connection', (socket) => {
127
128 this._renderScreen(socket);
129 });
130 }
131
132 // Obtains viewport size, and initializes a random screen with a random
133 // renderer, setting the required interval to draw.
134
135 _renderScreen(socket) {
136
137 const connectionData = {
138 width: null,
139 height: null,
140 modulation: 0,
141 screen: this._getScreen(),
142 renderer: this._getRenderer(),
143 socket
144 };
145 const interval = null;
146
147 socket.write(internals.kNAWSRequest);
148
149 socket.on('data', (data) => {
150
151 if (data.slice(0, 6).compare(internals.kNAWSResponse) === 0) {
152 connectionData.width = Util.parse16BitBuffer(data.slice(6, 8));
153 connectionData.height = Util.parse16BitBuffer(data.slice(8, 10));
154
155 socket.write('\x1B[2J'); // Clear the Screen (CSI 2 J)
156 interval = setInterval(this._writeMessage.bind(this, connectionData), this.frequency);
157 }
158
159 if (data.compare(internals.kEscape) === 0) {
160 socket.write('\n');
161 clearInterval(interval);
162 socket.end();
163 }
164 });
165 }
166
167 // Resets the cursor, gets a frame and sends it to the socket.
168
169 _writeMessage(connectionData) {
170
171 const payload = connectionData.screen(connectionData.modulation, connectionData.width, connectionData.height, connectionData.renderer);
172 const message = `\x1B[1;1H${payload}`; // reset cursor position before payload
173
174 connectionData.modulation = (connectionData.modulation + this.modulation) % 256;
175 connectionData.socket.write(message);
176 }
177
178 _getScreen() {
179
180 return Util.pickRandom(this.screens);
181 }
182
183 _getRenderer() {
184
185 return Util.pickRandom(this.renderers);
186 }
187 };
188
189 module.exports = TomatoSauce;