]>
Commit | Line | Data |
---|---|---|
c7b4bd19 BB |
1 | 'use strict'; |
2 | ||
3 | const Net = require('net'); | |
4 | const EventEmitter = require('events'); | |
5 | ||
6 | const Util = require('./util'); | |
7 | ||
fd38d409 | 8 | const internals = { |
a0666be3 RBR |
9 | // Interpret as Command Sequences. |
10 | kEscape: Buffer.from([0xFF, 0xF4, 0XFF, 0xFD, 0x06]), // IAC IP IAC DO TIMING_MARK | |
11 | kNAWSRequest: Buffer.from([0xFF, 0xFD, 0X1F]), // IAC DO NAWS | |
12 | kNAWSResponse: Buffer.from([0xFF, 0xFB, 0X1F, 0xFF, 0xFA, 0X1F]) // IAC WILL NAWS IAC SB NAWS | |
fd38d409 RBR |
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 | */ | |
c7b4bd19 BB |
64 | const TomatoSauce = class TomatoSauce extends EventEmitter { |
65 | ||
a0666be3 | 66 | constructor(config = {}) { |
fd38d409 | 67 | |
a0666be3 | 68 | super(); |
c7b4bd19 | 69 | |
a0666be3 RBR |
70 | this.screens = []; |
71 | this.renderers = []; | |
c7b4bd19 | 72 | |
a0666be3 RBR |
73 | // Defaults. |
74 | this.port = 9999; | |
75 | this.frequency = 333; | |
76 | this.modulation = 5; | |
fd38d409 | 77 | |
a0666be3 RBR |
78 | Object.assign(this, config); |
79 | } | |
c7b4bd19 | 80 | |
a0666be3 | 81 | /** |
fd38d409 RBR |
82 | * Main entry point, initializes the server and binds events for connections |
83 | * | |
84 | * @function run | |
85 | * @instance | |
86 | * @memberof TomatoSauce | |
87 | */ | |
a0666be3 | 88 | run() { |
fd38d409 | 89 | |
a0666be3 RBR |
90 | this._startServer(); |
91 | this._bindEvents(); | |
92 | } | |
c7b4bd19 | 93 | |
a0666be3 RBR |
94 | // Creates a socket, server based on the configuration. Emits the |
95 | // listening event when ready. | |
fd38d409 | 96 | |
a0666be3 | 97 | _startServer() { |
fd38d409 | 98 | |
a0666be3 RBR |
99 | const server = Net.createServer(); |
100 | this.server = server; | |
101 | server.listen(this.port, () => { | |
fd38d409 | 102 | |
a0666be3 RBR |
103 | this.emit('listening', { |
104 | data: { | |
105 | server | |
106 | } | |
107 | }); | |
108 | }); | |
109 | } | |
c7b4bd19 | 110 | |
a0666be3 | 111 | // Binds the connection and error events |
fd38d409 | 112 | |
a0666be3 | 113 | _bindEvents() { |
fd38d409 | 114 | |
a0666be3 RBR |
115 | // Send the error event all the way up. |
116 | this.server.on('error', (error) => { | |
fd38d409 | 117 | |
a0666be3 RBR |
118 | this.emit('error', { |
119 | data: { | |
120 | error | |
121 | } | |
122 | }); | |
123 | }); | |
c7b4bd19 | 124 | |
a0666be3 RBR |
125 | // Send the error event all the way up. |
126 | this.server.on('connection', (socket) => { | |
fd38d409 | 127 | |
a0666be3 RBR |
128 | this._renderScreen(socket); |
129 | }); | |
130 | } | |
c7b4bd19 | 131 | |
a0666be3 RBR |
132 | // Obtains viewport size, and initializes a random screen with a random |
133 | // renderer, setting the required interval to draw. | |
fd38d409 | 134 | |
a0666be3 | 135 | _renderScreen(socket) { |
fd38d409 | 136 | |
a0666be3 RBR |
137 | const connectionData = { |
138 | width: null, | |
139 | height: null, | |
140 | modulation: 0, | |
141 | screen: this._getScreen(), | |
142 | renderer: this._getRenderer(), | |
143 | socket | |
144 | }; | |
145 | let interval = null; | |
fd38d409 | 146 | |
a0666be3 | 147 | socket.write(internals.kNAWSRequest); |
c7b4bd19 | 148 | |
a0666be3 | 149 | socket.on('data', (data) => { |
c7b4bd19 | 150 | |
a0666be3 RBR |
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)); | |
c7b4bd19 | 154 | |
a0666be3 RBR |
155 | socket.write('\x1B[2J'); // Clear the Screen (CSI 2 J) |
156 | interval = setInterval(this._writeMessage.bind(this, connectionData), this.frequency); | |
157 | } | |
c7b4bd19 | 158 | |
a0666be3 RBR |
159 | if (data.compare(internals.kEscape) === 0) { |
160 | socket.write('\n'); | |
161 | clearInterval(interval); | |
162 | socket.end(); | |
163 | } | |
164 | }); | |
165 | } | |
c7b4bd19 | 166 | |
a0666be3 | 167 | // Resets the cursor, gets a frame and sends it to the socket. |
fd38d409 | 168 | |
a0666be3 | 169 | _writeMessage(connectionData) { |
fd38d409 | 170 | |
a0666be3 RBR |
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 | |
c7b4bd19 | 173 | |
a0666be3 RBR |
174 | connectionData.modulation = (connectionData.modulation + this.modulation) % 256; |
175 | connectionData.socket.write(message); | |
176 | } | |
c7b4bd19 | 177 | |
a0666be3 | 178 | _getScreen() { |
fd38d409 | 179 | |
a0666be3 RBR |
180 | return Util.pickRandom(this.screens); |
181 | } | |
c7b4bd19 | 182 | |
a0666be3 | 183 | _getRenderer() { |
fd38d409 | 184 | |
a0666be3 RBR |
185 | return Util.pickRandom(this.renderers); |
186 | } | |
c7b4bd19 BB |
187 | }; |
188 | ||
c7b4bd19 | 189 | module.exports = TomatoSauce; |