]>
Commit | Line | Data |
---|---|---|
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: 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 | |
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 | let 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; |