From: Ben Beltran Date: Mon, 9 May 2016 05:31:52 +0000 (-0500) Subject: Merge branch 'release/1.0.0' X-Git-Tag: 1.0.0~1 X-Git-Url: https://git.r.bdr.sh/rbdr/tomato-sauce/commitdiff_plain/a41763dd463339765081c1f968a7b93b8ccf572f?hp=95da493e0ec223e1c7706fc0bdc364bd3a21d999 Merge branch 'release/1.0.0' --- diff --git a/0001_BASIC.JS b/0001_BASIC.JS deleted file mode 100755 index 54309d9..0000000 --- a/0001_BASIC.JS +++ /dev/null @@ -1,245 +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)); - - console.log(colorOffset); - - 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(); diff --git a/README.md b/README.md index 597eb0f..87f1ae5 100644 --- 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 , width , height , renderer + ) -> payload ` + +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 , green , blue ) -> + colorString ` + +## 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 index 0000000..5a416d5 --- /dev/null +++ b/bin/tomato_sauce.js @@ -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 index 0000000..e8e6e37 --- /dev/null +++ b/lib/renderers/256_colors.js @@ -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 index 0000000..c3975da --- /dev/null +++ b/lib/renderers/ansi.js @@ -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 index 0000000..b82bb45 --- /dev/null +++ b/lib/renderers/fake_color.js @@ -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 index 0000000..5f7725f --- /dev/null +++ b/lib/renderers/true_color.js @@ -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 index 0000000..d8a4d10 --- /dev/null +++ b/lib/screens/circle.js @@ -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 index 0000000..5e63d5b --- /dev/null +++ b/lib/screens/gradients.js @@ -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 index 0000000..d683fb6 --- /dev/null +++ b/lib/screens/mirrors.js @@ -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 index 0000000..6ac706f --- /dev/null +++ b/lib/screens/random.js @@ -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 index 0000000..446f46e --- /dev/null +++ b/lib/screens/sprinkles.js @@ -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 index 0000000..0ff087a --- /dev/null +++ b/lib/tomato_sauce.js @@ -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 +// * renderers +// * port +// * frequency +// * modulation +// +// 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 index 0000000..3234d3e --- /dev/null +++ b/lib/util.js @@ -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 index 0000000..908aa05 --- /dev/null +++ b/package.json @@ -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 ", + "license": "MIT", + "bugs": { + "url": "https://github.com/rbdr/tomato-sauce/issues" + }, + "homepage": "https://github.com/rbdr/tomato-sauce#readme", + "dependencies": { + "getenv": "^0.6.0" + } +}