]>
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 RBR |
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 | */ | |
c7b4bd19 BB |
64 | const TomatoSauce = class TomatoSauce extends EventEmitter { |
65 | ||
fd38d409 RBR |
66 | constructor(config = {}) { |
67 | ||
c7b4bd19 BB |
68 | super(); |
69 | ||
70 | this.screens = []; | |
71 | this.renderers = []; | |
72 | ||
fd38d409 RBR |
73 | // Defaults. |
74 | this.port = 9999; | |
75 | this.frequency = 333; | |
76 | this.modulation = 5; | |
77 | ||
78 | Object.assign(this, config); | |
c7b4bd19 BB |
79 | } |
80 | ||
fd38d409 RBR |
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 | ||
c7b4bd19 BB |
90 | this._startServer(); |
91 | this._bindEvents(); | |
92 | } | |
93 | ||
94 | // Creates a socket, server based on the configuration. Emits the | |
95 | // listening event when ready. | |
fd38d409 RBR |
96 | |
97 | _startServer() { | |
98 | ||
c7b4bd19 BB |
99 | const server = Net.createServer(); |
100 | this.server = server; | |
fd38d409 RBR |
101 | server.listen(this.port, () => { |
102 | ||
c7b4bd19 BB |
103 | this.emit('listening', { |
104 | data: { | |
fd38d409 | 105 | server |
c7b4bd19 BB |
106 | } |
107 | }); | |
fd38d409 | 108 | }); |
c7b4bd19 BB |
109 | } |
110 | ||
fd38d409 RBR |
111 | // Binds the connection and error events |
112 | ||
113 | _bindEvents() { | |
114 | ||
c7b4bd19 | 115 | // Send the error event all the way up. |
fd38d409 RBR |
116 | this.server.on('error', (error) => { |
117 | ||
c7b4bd19 BB |
118 | this.emit('error', { |
119 | data: { | |
fd38d409 | 120 | error |
c7b4bd19 BB |
121 | } |
122 | }); | |
fd38d409 | 123 | }); |
c7b4bd19 BB |
124 | |
125 | // Send the error event all the way up. | |
fd38d409 RBR |
126 | this.server.on('connection', (socket) => { |
127 | ||
c7b4bd19 | 128 | this._renderScreen(socket); |
fd38d409 | 129 | }); |
c7b4bd19 BB |
130 | } |
131 | ||
132 | // Obtains viewport size, and initializes a random screen with a random | |
133 | // renderer, setting the required interval to draw. | |
fd38d409 RBR |
134 | |
135 | _renderScreen(socket) { | |
136 | ||
137 | const connectionData = { | |
c7b4bd19 BB |
138 | width: null, |
139 | height: null, | |
140 | modulation: 0, | |
141 | screen: this._getScreen(), | |
142 | renderer: this._getRenderer(), | |
fd38d409 | 143 | socket |
c7b4bd19 | 144 | }; |
36ff43dc | 145 | let interval = null; |
fd38d409 RBR |
146 | |
147 | socket.write(internals.kNAWSRequest); | |
c7b4bd19 | 148 | |
fd38d409 | 149 | socket.on('data', (data) => { |
c7b4bd19 | 150 | |
fd38d409 | 151 | if (data.slice(0, 6).compare(internals.kNAWSResponse) === 0) { |
c7b4bd19 BB |
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 | ||
fd38d409 | 159 | if (data.compare(internals.kEscape) === 0) { |
c7b4bd19 BB |
160 | socket.write('\n'); |
161 | clearInterval(interval); | |
162 | socket.end(); | |
163 | } | |
fd38d409 | 164 | }); |
c7b4bd19 BB |
165 | } |
166 | ||
167 | // Resets the cursor, gets a frame and sends it to the socket. | |
fd38d409 RBR |
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 | |
c7b4bd19 BB |
173 | |
174 | connectionData.modulation = (connectionData.modulation + this.modulation) % 256; | |
175 | connectionData.socket.write(message); | |
176 | } | |
177 | ||
fd38d409 RBR |
178 | _getScreen() { |
179 | ||
c7b4bd19 BB |
180 | return Util.pickRandom(this.screens); |
181 | } | |
182 | ||
fd38d409 RBR |
183 | _getRenderer() { |
184 | ||
c7b4bd19 BB |
185 | return Util.pickRandom(this.renderers); |
186 | } | |
187 | }; | |
188 | ||
c7b4bd19 | 189 | module.exports = TomatoSauce; |