]> git.r.bdr.sh - rbdr/tomato-sauce/commitdiff
Merge branch 'feature/the-actual-tomato-sauce' into develop
authorBen Beltran <redacted>
Mon, 9 May 2016 05:31:25 +0000 (00:31 -0500)
committerBen Beltran <redacted>
Mon, 9 May 2016 05:31:25 +0000 (00:31 -0500)
15 files changed:
0001_BASIC.JS [deleted file]
README.md
bin/tomato_sauce.js [new file with mode: 0755]
lib/renderers/256_colors.js [new file with mode: 0644]
lib/renderers/ansi.js [new file with mode: 0644]
lib/renderers/fake_color.js [new file with mode: 0644]
lib/renderers/true_color.js [new file with mode: 0644]
lib/screens/circle.js [new file with mode: 0644]
lib/screens/gradients.js [new file with mode: 0644]
lib/screens/mirrors.js [new file with mode: 0644]
lib/screens/random.js [new file with mode: 0644]
lib/screens/sprinkles.js [new file with mode: 0644]
lib/tomato_sauce.js [new file with mode: 0644]
lib/util.js [new file with mode: 0644]
package.json [new file with mode: 0644]

diff --git a/0001_BASIC.JS b/0001_BASIC.JS
deleted file mode 100755 (executable)
index 0492ec2..0000000
+++ /dev/null
@@ -1,243 +0,0 @@
-#!/usr/bin/env node
-
-'use strict';
-
-/* eslint no-console: 0 */
-
-const Net = require('net');
-
-const kPort = 9999;
-const kEscape = new Buffer([0xFF, 0xF4, 0XFF, 0xFD, 0x06]); // IAC IP IAC DO TIMING_MARK
-const kNAWSRequest = new Buffer([0xFF, 0xFD, 0X1F]); // IAC DO NAWS
-const kNAWSResponse = new Buffer([0xFF, 0xFB, 0X1F, 0xFF, 0xFA, 0X1F]); // IAC WILL NAWS IAC SB NAWS
-const kFrequency = 333;
-const kModulationSpeed = 5;
-
-const run = function run () {
-  const server = startServer();
-  bindEvents(server);
-};
-
-const startServer = function startServer () {
-  const server = Net.createServer();
-  server.listen(kPort, function () {
-    const address = server.address();
-    console.log(`The server is now listening on ${address.address}:${address.port}`);
-  });
-
-  return server;
-};
-
-const bindEvents = function bindEvents (server) {
-  server.on('error', function (err) {
-    console.error(err.stack || err.message || err);
-    process.exit(1);
-  });
-
-  server.on('connection', function (socket) {
-    let connectionData = {
-      width: null,
-      height: null,
-      modulation: 0,
-      screen: screens[Math.floor(Math.random() * screens.length)],
-      mode: modes[Math.floor(Math.random() * modes.length)]
-    };
-    let interval = null;
-
-    socket.write(kNAWSRequest);
-
-    // Currently this is naive, we should parse the buffer properly.
-    socket.on('data', function (data) {
-
-      // Only send message if NAWS is supported.
-      if (data.slice(0, 6).compare(kNAWSResponse) === 0) {
-        connectionData.width = parse16BitBuffer(data.slice(6, 8));
-        connectionData.height = parse16BitBuffer(data.slice(8, 10));
-
-        socket.write('\x1B[2J'); // Clear the Screen (CSI 2 J)
-        interval = setInterval(writeMessage.bind(null, socket, connectionData), kFrequency);
-      }
-
-      if (data.compare(kEscape) === 0) {
-        socket.write('\n');
-        clearInterval(interval);
-        socket.end();
-      }
-    });
-  });
-};
-
-const writeMessage = function writeMessage (socket, connectionData) {
-  let message = '';
-  message += '\x1B[1;1H'; // Move cursor to top left (CSI 1;1 H)
-  message += connectionData.screen(connectionData.modulation, connectionData.width, connectionData.height, getANSIColor.bind(null, connectionData.mode));
-
-  connectionData.modulation = connectionData.modulation + kModulationSpeed;
-  if (connectionData.modulation === 255) {
-    connectionData.modulation = 0;
-  }
-
-  socket.write(message);
-  // return '\x1B[2J*** SUCCESSS ***';
-};
-
-const parse16BitBuffer = function parse16BitBuffer (buffer) {
-  return buffer[0] * 256 + buffer[1];
-};
-
-// SCREENS
-
-const fillScreen = function fillScreen (modulation, width, height, getANSIColor) {
-  let response = '';
-
-  for (let i = 0; i < height; i++) {
-    for (let j = 0; j < width; j++) {
-      let red = ((modulation + i) * 255 / height) % 255;
-      let blue = ((modulation + j) * 255 / width) % 255;
-      let green = ((modulation + i * j) * 255 / (width * height)) % 255;
-
-      response = response + getANSIColor(red, blue, green);
-      response = response + ' ';
-    }
-
-    if (i < height - 1) {
-      response = response + '\n';
-    }
-  }
-
-  return response;
-};
-
-const randomScreen = function randomScreen (modulation, width, height, getANSIColor) {
-  let response = '';
-
-  for (let i = 0; i < height; i++) {
-    for (let j = 0; j < width; j++) {
-      let red = Math.floor(Math.random() * 255);
-      let blue = Math.floor(Math.random() * 255);
-      let green = Math.floor(Math.random() * 255);
-
-      response = response + getANSIColor(red, blue, green);
-      response = response + ' ';
-    }
-
-    if (i < height - 1) {
-      response = response + '\n';
-    }
-  }
-
-  return response;
-};
-
-const mirrorScreen = function mirrorScreen (modulation, width, height, getANSIColor) {
-  let response = [];
-
-  let scale = 2 + Math.round(Math.random() * 4);
-  let scaledHeight = Math.floor(height / scale);
-  let scaledWidth = Math.floor(width / scale);
-
-  for (let i = 0; i < scaledHeight; i++) {
-    let row = [];
-    for (let j = 0; j < scaledWidth; j++) {
-      let red = ((modulation + i) * 255 / height) % 255;
-      let blue = ((modulation + j) * 255 / width) % 255;
-      let green = ((modulation + i * j) * 255 / (width * height)) % 255;
-
-      let cell = [getANSIColor(red, blue, green), ' '];
-      row.push(cell.join(''));
-    }
-
-    let rowText = '';
-    for (let j = 0; j < scale; j++) {
-      rowText += row.reverse().join('');
-    }
-
-    for (let j = 1; j < scale; j++) {
-      response[j * i] = rowText;
-      response[(height - 1 - (j * i))] = rowText;
-    }
-  }
-
-  return response.join('\n');
-};
-
-const randomSprinkles = function mirrorRandomScreen (modulation, width, height, getANSIColor) {
-  let response = '';
-
-  let maxSprinkleCount = (width * height) / 2;
-  let minSprinkleCount = (width * height) / 8;
-  let sprinkleCount = Math.round(Math.random() * (maxSprinkleCount - minSprinkleCount)) + minSprinkleCount;
-
-  let red = Math.floor(Math.random() * 255);
-  let blue = Math.floor(Math.random() * 255);
-  let green = Math.floor(Math.random() * 255);
-
-  for (let i = 0; i < sprinkleCount; i++) {
-    let x = Math.round(Math.random() * (width - 1)) + 1;
-    let y = Math.round(Math.random() * (height - 1)) + 1;
-
-    let position = `\x1B[${y};${x}H`; // Move cursor to y,x (CSI y;x H)
-
-    response += `${position}${getANSIColor(red, blue, green)} `;
-  }
-
-  return response;
-};
-
-const circlesScreen = function circlesScreen (modulation, width, height, getANSIColor) {
-  let response = [];
-
-  let circles = width > height ? height : width;
-
-  for (let i = 0; i < circles; i++) {
-    let centerX = Math.round(width / 2) + 1;
-    let centerY = Math.round(height / 2) + 1;
-
-    let red = Math.floor(Math.random() * 255);
-    let blue = Math.floor(Math.random() * 255);
-    let green = Math.floor(Math.random() * 255);
-
-    for (let j = 0; j < 180; j++) {
-      let angle =  2 * j * (Math.PI / 180);
-      let x = Math.round(centerX + Math.sin(angle) * i);
-      let y = Math.round(centerY + Math.cos(angle) * i);
-
-      if (x <= width && x > 0 && y <= height && y > 0) {
-        let position = `\x1B[${y};${x}H`; // Move cursor to y,x (CSI y;x H)
-        response += `${position}${getANSIColor(red, blue, green)} `;
-      }
-    }
-  }
-
-  return response;
-};
-
-// instead of binding themode, return the one funciton...
-const getANSIColor = function getANSIColor (mode, red, blue, green) {
-  let colorCode;
-
-  if (mode === '256') {
-    let redValue = Math.round(red * 5 / 255);
-    let blueValue = Math.round(blue * 5 / 255);
-    let greenValue = Math.round(green * 5 / 255);
-
-    let colorNumber = 16 + 36 * redValue + 6 * greenValue + blueValue;
-
-    colorCode = `\x1B[48;5;${colorNumber}m`;
-  } else if (mode === 'ansi') {
-    let colorOffset = Math.round((red + blue + green) * 7 / (255 * 3));
-
-    let colorNumber = 40 + colorOffset;
-
-    colorCode = `\x1B[${colorNumber}m`;
-  } else{
-    colorCode = `\x1B[48;2;${Math.round(red)};${Math.round(green)};${Math.round(blue)}m`;
-  }
-
-  return colorCode;
-};
-
-const screens = [fillScreen, randomScreen, mirrorScreen, randomSprinkles, circlesScreen];
-const modes = ['full', '256', 'ansi'];
-
-run();
index 597eb0fa629358ba989728ca725ac62b98114d4b..87f1ae5cd71902e054384d251dbb23900b6606e9 100644 (file)
--- a/README.md
+++ b/README.md
@@ -1,2 +1,88 @@
 # tomato-sauce
 Draw stuff via telnet
+
+## How to run
+
+`npm install` and then run ``./bin/server.js`. It will listen on port 9999
+by default.
+
+## Configuration variables
+
+Tomato Sauce can be configured using env variables:
+
+* `TOMATO_SAUCE_PORT`: The port to listen on. Defauts to 9999.
+* `TOMATO_SAUCE_FREQUENCY`: How often it will send a screen in
+  miliseconds. Defaults to 333.
+* `TOMATO_SAUCE_MODULATION`: How much the modulation counter will
+  increase per round. Defaults to 5.
+* `TOMATO_SAUCE_SCREEN_PATH`: Path from which we will load screens. It
+  defaults to `{project_root}/lib/screens`
+* `TOMATO_SAUCE_RENDERER_PATH`: Path from which we will load renderers. It
+  defaults to `{project_root}/lib/renderers`
+
+## Make your own screens
+
+A screen is a function that will receive a modulation value from 0-255,
+the width of the viewport, the height of the viewport, and a renderer
+function, and it returns a string that consists of the commands that
+will be sent to the socket.
+
+  * `TomatoSauce.IScreen(modulation <int>, width <int>, height <int>, renderer
+    <TomatoSauce.IRenderer>) -> payload <string>`
+
+It should output the required commands that telnet needs to move the
+cursor and draw. For convenience, a renderer function is passed so
+calculating color should not be a part of the screen, just moving the
+cursor around and calling specific colors.
+
+## Make your own renderers
+
+The included renderers are wrappers to some common ways of obtaining
+colors in the terminal: ANSI, 256 colors, 24-bit colors, and a fake
+color string that uses incorrect color strings to generate random
+variations.
+
+You can build your own renderer by building a function that receives a
+red, green, and blue component from 0 to 255 and returns the escape
+codes to generate the color.
+
+  * `TomatoSauce.IRenderer(red <int>, green <int>, blue <int>) ->
+    colorString <string>`
+
+## Using as a library
+
+The binary just serves as a wrapper to read configuration, and as a
+bridge to the console. It consumes `lib/tomato_sauce`. All configuration
+is optional, and should be passed as an object on instantiation. The
+instance is an event emitter that will emit a `listening` event with
+the server data, and an `error` event in case something goes wrong.
+
+```
+const TomatoSauce = require('tomato-sauce');
+
+const tomatoSauce = new TomatoSauce(config);
+
+tomatoSauce.on('listening', function () {
+  const address = event.data.server.address();
+  console.log(`Tomato Sauce listening on: ${address.address}:${address.port}`);
+});
+
+tomatoSauce.on('error', function (error) {
+  console.error(error);
+});
+
+tomatoSauce.run();
+
+### Configuration Values
+
+The config object should be a simple object with these keys (All are
+optional.)
+
+  * `port`: The port to listen on (Defaults to 9999)
+  * `frequency`: How often we'll send a new frame in milliseconds
+    (Defaults to 333)
+  * `modulation`: How fast the modulation counter will be increased
+    (Defaults to 5)
+  * `screens`: An array containing the screen functions (Defaults to [])
+  * `renderers`: An array containing the renderer functions (Defaults to [])
+```
diff --git a/bin/tomato_sauce.js b/bin/tomato_sauce.js
new file mode 100755 (executable)
index 0000000..5a416d5
--- /dev/null
@@ -0,0 +1,41 @@
+#!/usr/bin/env node
+
+'use strict';
+
+/* eslint no-console: 0 */
+
+const Path = require('path');
+const Getenv = require('getenv');
+
+const TomatoSauce = require('..');
+const Util = require('../lib/util');
+
+const config = {
+  port: Getenv.int('TOMATO_SAUCE_PORT', 9999),
+  frequency: Getenv.int('TOMATO_SAUCE_FREQUENCY', 333),
+  modulation: Getenv.int('TOMATO_SAUCE_MODULATION_SPEED', 5)
+};
+
+const screenPath = Getenv('TOMATO_SAUCE_SCREEN_PATH', Path.join(__dirname, '../lib/screens'));
+const rendererPath = Getenv('TOMATO_SAUCE_RENDERER_PATH', Path.join(__dirname, '../lib/renderers'));
+
+Util.loadFiles(screenPath).then(function (screens) {
+  config.screens = screens;
+  return Util.loadFiles(rendererPath);
+}).then(function (renderers) {
+  config.renderers = renderers;
+}).then(function () {
+  let tomatoSauce = new TomatoSauce(config);
+
+  tomatoSauce.on('listening', function (event) {
+    const address = event.data.server.address();
+    console.log(`Tomato Sauce listening on: ${address.address}:${address.port}`);
+  });
+
+  tomatoSauce.on('error', function (error) {
+    console.error(error.stack || error.message || error);
+    process.exit(1);
+  });
+
+  tomatoSauce.run();
+});
diff --git a/lib/renderers/256_colors.js b/lib/renderers/256_colors.js
new file mode 100644 (file)
index 0000000..e8e6e37
--- /dev/null
@@ -0,0 +1,16 @@
+'use strict';
+
+// Returns a 256 color, see
+// https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
+// under 256 colors for more info.
+const TwoFiftySixColors = function (red, blue, green) {
+  let redValue = Math.round(red * 5 / 255);
+  let blueValue = Math.round(blue * 5 / 255);
+  let greenValue = Math.round(green * 5 / 255);
+
+  let colorNumber = 16 + 36 * redValue + 6 * greenValue + blueValue;
+
+  return `\x1B[48;5;${colorNumber}m`;
+};
+
+module.exports = TwoFiftySixColors;
diff --git a/lib/renderers/ansi.js b/lib/renderers/ansi.js
new file mode 100644 (file)
index 0000000..c3975da
--- /dev/null
@@ -0,0 +1,13 @@
+'use strict';
+
+// Returns a basic ANSI color. See the color table in:
+// https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
+const ANSI = function (red, blue, green) {
+  let colorOffset = Math.round((red + blue + green) * 7 / (255 * 3));
+
+  let colorNumber = 40 + colorOffset;
+
+  return `\x1B[${colorNumber}m`;
+};
+
+module.exports = ANSI;
diff --git a/lib/renderers/fake_color.js b/lib/renderers/fake_color.js
new file mode 100644 (file)
index 0000000..b82bb45
--- /dev/null
@@ -0,0 +1,8 @@
+'use strict';
+
+// Sends malformed ANSI 24-bit color strings
+const FakeColor = function (red, blue, green) {
+  return `\x1B[28;2;${Math.round(red)};${Math.round(green)};${Math.round(blue)}m`;
+};
+
+module.exports = FakeColor;
diff --git a/lib/renderers/true_color.js b/lib/renderers/true_color.js
new file mode 100644 (file)
index 0000000..5f7725f
--- /dev/null
@@ -0,0 +1,11 @@
+'use strict';
+
+// Returns an ANSI Code for True Color, see:
+// https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
+// and look for 24-bit colors. Only looks good in supported terminals,
+// otherwise looks like FakeColor.
+const TrueColor = function (red, blue, green) {
+  return `\x1B[48;2;${Math.round(red)};${Math.round(green)};${Math.round(blue)}m`;
+};
+
+module.exports = TrueColor;
diff --git a/lib/screens/circle.js b/lib/screens/circle.js
new file mode 100644 (file)
index 0000000..d8a4d10
--- /dev/null
@@ -0,0 +1,32 @@
+'use strict';
+
+// Draws concentric circles. Each ring has its own color.
+const Circle = function (modulation, width, height, renderer) {
+  let response = [];
+
+  let circles = width > height ? height : width;
+
+  for (let i = 0; i < circles; i++) {
+    let centerX = Math.round(width / 2) + 1;
+    let centerY = Math.round(height / 2) + 1;
+
+    let red = Math.floor(Math.random() * 255);
+    let blue = Math.floor(Math.random() * 255);
+    let green = Math.floor(Math.random() * 255);
+
+    for (let j = 0; j < 180; j++) {
+      let angle =  2 * j * (Math.PI / 180);
+      let x = Math.round(centerX + Math.sin(angle) * i);
+      let y = Math.round(centerY + Math.cos(angle) * i);
+
+      if (x <= width && x > 0 && y <= height && y > 0) {
+        let position = `\x1B[${y};${x}H`; // Move cursor to y,x (CSI y;x H)
+        response += `${position}${renderer(red, blue, green)} `;
+      }
+    }
+  }
+
+  return response;
+};
+
+module.exports = Circle;
diff --git a/lib/screens/gradients.js b/lib/screens/gradients.js
new file mode 100644 (file)
index 0000000..5e63d5b
--- /dev/null
@@ -0,0 +1,25 @@
+'use strict';
+
+// Draws moving gradient boxes.
+const Gradients = function (modulation, width, height, renderer) {
+  let response = '';
+
+  for (let i = 0; i < height; i++) {
+    for (let j = 0; j < width; j++) {
+      let red = ((modulation + i) * 255 / height) % 255;
+      let blue = ((modulation + j) * 255 / width) % 255;
+      let green = ((modulation + i * j) * 255 / (width * height)) % 255;
+
+      response = response + renderer(red, blue, green);
+      response = response + ' ';
+    }
+
+    if (i < height - 1) {
+      response = response + '\n';
+    }
+  }
+
+  return response;
+};
+
+module.exports = Gradients;
diff --git a/lib/screens/mirrors.js b/lib/screens/mirrors.js
new file mode 100644 (file)
index 0000000..d683fb6
--- /dev/null
@@ -0,0 +1,36 @@
+'use strict';
+
+// Draws small moving gradient boxes and repeats them.
+const Mirrors = function (modulation, width, height, renderer) {
+  let response = [];
+
+  let scale = 2 + Math.round(Math.random() * 4);
+  let scaledHeight = Math.floor(height / scale);
+  let scaledWidth = Math.floor(width / scale);
+
+  for (let i = 0; i < scaledHeight; i++) {
+    let row = [];
+    for (let j = 0; j < scaledWidth; j++) {
+      let red = ((modulation + i) * 255 / height) % 255;
+      let blue = ((modulation + j) * 255 / width) % 255;
+      let green = ((modulation + i * j) * 255 / (width * height)) % 255;
+
+      let cell = [renderer(red, blue, green), ' '];
+      row.push(cell.join(''));
+    }
+
+    let rowText = '';
+    for (let j = 0; j < scale; j++) {
+      rowText += row.reverse().join('');
+    }
+
+    for (let j = 1; j < scale; j++) {
+      response[j * i] = rowText;
+      response[(height - 1 - (j * i))] = rowText;
+    }
+  }
+
+  return response.join('\n');
+};
+
+module.exports = Mirrors;
diff --git a/lib/screens/random.js b/lib/screens/random.js
new file mode 100644 (file)
index 0000000..6ac706f
--- /dev/null
@@ -0,0 +1,25 @@
+'use strict';
+
+// random colors
+const Random = function (modulation, width, height, renderer) {
+  let response = '';
+
+  for (let i = 0; i < height; i++) {
+    for (let j = 0; j < width; j++) {
+      let red = Math.floor(Math.random() * 255);
+      let blue = Math.floor(Math.random() * 255);
+      let green = Math.floor(Math.random() * 255);
+
+      response = response + renderer(red, blue, green);
+      response = response + ' ';
+    }
+
+    if (i < height - 1) {
+      response = response + '\n';
+    }
+  }
+
+  return response;
+};
+
+module.exports = Random;
diff --git a/lib/screens/sprinkles.js b/lib/screens/sprinkles.js
new file mode 100644 (file)
index 0000000..446f46e
--- /dev/null
@@ -0,0 +1,28 @@
+'use strict';
+
+// Places random sprinkles in the screen each frame. Same color per
+// frame.
+const Sprinkles = function (modulation, width, height, renderer) {
+  let response = '';
+
+  let maxSprinkleCount = (width * height) / 2;
+  let minSprinkleCount = (width * height) / 8;
+  let sprinkleCount = Math.round(Math.random() * (maxSprinkleCount - minSprinkleCount)) + minSprinkleCount;
+
+  let red = Math.floor(Math.random() * 255);
+  let blue = Math.floor(Math.random() * 255);
+  let green = Math.floor(Math.random() * 255);
+
+  for (let i = 0; i < sprinkleCount; i++) {
+    let x = Math.round(Math.random() * (width - 1)) + 1;
+    let y = Math.round(Math.random() * (height - 1)) + 1;
+
+    let position = `\x1B[${y};${x}H`; // Move cursor to y,x (CSI y;x H)
+
+    response += `${position}${renderer(red, blue, green)} `;
+  }
+
+  return response;
+};
+
+module.exports = Sprinkles;
diff --git a/lib/tomato_sauce.js b/lib/tomato_sauce.js
new file mode 100644 (file)
index 0000000..0ff087a
--- /dev/null
@@ -0,0 +1,129 @@
+'use strict';
+
+const Net = require('net');
+const EventEmitter = require('events');
+
+const Util = require('./util');
+
+// Interpret as Command Sequences.
+const kEscape = new Buffer([0xFF, 0xF4, 0XFF, 0xFD, 0x06]); // IAC IP IAC DO TIMING_MARK
+const kNAWSRequest = new Buffer([0xFF, 0xFD, 0X1F]); // IAC DO NAWS
+const kNAWSResponse = new Buffer([0xFF, 0xFB, 0X1F, 0xFF, 0xFA, 0X1F]); // IAC WILL NAWS IAC SB NAWS
+
+// Main tomato sauce class. Properties:
+// * screens <TomatoSauce.IScreen[]>
+// * renderers <TomatoSauce.IRenderer[]>
+// * port <int>
+// * frequency <int>
+// * modulation <int>
+//
+// 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.
+const TomatoSauce = class TomatoSauce extends EventEmitter {
+
+  constructor (config) {
+    super();
+
+    this.screens = [];
+    this.renderers = [];
+
+    Object.assign(this, config || {});
+  }
+
+  // Here's where things get started.
+  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, function () {
+      this.emit('listening', {
+        data: {
+          server: server
+        }
+      });
+    }.bind(this));
+  }
+
+  _bindEvents () {
+    // Send the error event all the way up.
+    this.server.on('error', function (err) {
+      this.emit('error', {
+        data: {
+          error: err
+        }
+      });
+    }.bind(this));
+
+    // Send the error event all the way up.
+    this.server.on('connection', function (socket) {
+      this._renderScreen(socket);
+    }.bind(this));
+  }
+
+  // Obtains viewport size, and initializes a random screen with a random
+  // renderer, setting the required interval to draw.
+  _renderScreen (socket) {
+    let connectionData = {
+      width: null,
+      height: null,
+      modulation: 0,
+      screen: this._getScreen(),
+      renderer: this._getRenderer(),
+      socket: socket
+    };
+    let interval = null;
+
+    socket.write(kNAWSRequest);
+
+    socket.on('data', function (data) {
+      if (data.slice(0, 6).compare(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(kEscape) === 0) {
+        socket.write('\n');
+        clearInterval(interval);
+        socket.end();
+      }
+    }.bind(this));
+  }
+
+  // Resets the cursor, gets a frame and sends it to the socket.
+  _writeMessage (connectionData) {
+    let payload = connectionData.screen(connectionData.modulation, connectionData.width, connectionData.height, connectionData.renderer);
+    let 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);
+  }
+};
+
+// Defaults.
+TomatoSauce.prototype.port = 9999;
+TomatoSauce.prototype.frequency = 333;
+TomatoSauce.prototype.modulation = 5;
+
+module.exports = TomatoSauce;
diff --git a/lib/util.js b/lib/util.js
new file mode 100644 (file)
index 0000000..3234d3e
--- /dev/null
@@ -0,0 +1,50 @@
+'use strict';
+
+const Fs = require('fs');
+const Path = require('path');
+
+// Module containing utility functions.
+const Util = {
+
+  // Parses a 16 bit number buffer.
+  parse16BitBuffer: function (buffer) {
+    return buffer[0] * 256 + buffer[1];
+  },
+
+  // Picks a random element from an array.
+  pickRandom: function (array) {
+    return array[Math.floor(Math.random() * array.length)];
+  },
+
+  // For a given path, requires all of the files and returns an array
+  // with the results. If the directory contains any non-requireable file,
+  // it will fail.
+  loadFiles: function (path) {
+    return new Promise(function (resolve, reject) {
+      Fs.readdir(path, function (err, files) {
+        if (err) {
+          return reject(err);
+        }
+
+        let loadedFiles = [];
+
+        for (let file of files) {
+          let filePath = Path.join(path, file);
+          let loadedFile;
+
+          try {
+            loadedFile = require(filePath);
+          } catch (err) {
+            return reject(err);
+          }
+
+          loadedFiles.push(loadedFile);
+        }
+
+        resolve(loadedFiles);
+      });
+    });
+  }
+};
+
+module.exports = Util;
diff --git a/package.json b/package.json
new file mode 100644 (file)
index 0000000..908aa05
--- /dev/null
@@ -0,0 +1,29 @@
+{
+  "name": "tomato-sauce",
+  "version": "1.0.0",
+  "description": "\"Telnet based drawing functions and renderers\"",
+  "main": "lib/tomato_sauce.js",
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/rbdr/tomato-sauce.git"
+  },
+  "keywords": [
+    "tomato",
+    "sauce",
+    "telnet",
+    "ansi",
+    "colors"
+  ],
+  "author": "Rubén Beltrán del Río <ben@nsovocal.com>",
+  "license": "MIT",
+  "bugs": {
+    "url": "https://github.com/rbdr/tomato-sauce/issues"
+  },
+  "homepage": "https://github.com/rbdr/tomato-sauce#readme",
+  "dependencies": {
+    "getenv": "^0.6.0"
+  }
+}