...
 
Commits (2)
{
"extends": "eslint-config-hapi",
"rules": {
"indent": [
2,
2
],
"no-undef": 2,
"require-yield": 0
}
}
image: node:8
stages:
- lint
before_script:
- npm install
lint:
stage: lint
script:
- npm run lint
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
## [Unreleased]
### Added
- Contributing Guide
- This Changelog
- Linter
- CI Config
- Documentation with JSDoc
### Changed
- Code style
## 1.0.0 - 2016-05-09
### Added
- 256 color renderer
- ANSI color renderer
- True color renderer
- Fake color renderer
- Circle screen
- Gradients screen
- Random screen
- Sprinkles screen
- Mirrors screen
- Main server
[Unreleased]: https://gitlab.com/rbdr/sumo/compare/master...develop
# Contributing to Tomato Sauce
This project doesn't really have much of a roadmap other than making
more screens. If you have any new screens or would like to make
improvements please feel free to submit changes.
## The objective of Tomato Sauce
Render pretty things using telnet
## How to contribute
Above All: Be nice, always.
* Ensure the linter shows no warnings or errors
* Don't break the CI
* Make the PRs according to [Git Flow][gitflow]: (features go to
develop, hotfixes go to master)
[gitflow]: https://github.com/nvie/gitflow
# tomato-sauce
## Tomato Sauce
Draw stuff via telnet
## How to run
`npm install` and then run ``./bin/server.js`. It will listen on port 9999
You will need [Node.js][node] installed, at least version 8. Install
dependencies by running `npm install` from the root of the project
and start the server by running `npm start`. It will listen on port 9999
by default.
## Configuration variables
......@@ -27,8 +30,8 @@ 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>`
* `IScreen(modulation <int>, width <int>, height <int>, renderer
<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
......@@ -46,7 +49,7 @@ 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>) ->
* `IRenderer(red <int>, green <int>, blue <int>) ->
colorString <string>`
## Using as a library
......@@ -62,12 +65,14 @@ const TomatoSauce = require('tomato-sauce');
const tomatoSauce = new TomatoSauce(config);
tomatoSauce.on('listening', function () {
tomatoSauce.on('listening', () => {
const address = event.data.server.address();
console.log(`Tomato Sauce listening on: ${address.address}:${address.port}`);
});
tomatoSauce.on('error', function (error) {
tomatoSauce.on('error', (error) => {
console.error(error);
});
......@@ -86,4 +91,5 @@ optional.)
(Defaults to 5)
* `screens`: An array containing the screen functions (Defaults to [])
* `renderers`: An array containing the renderer functions (Defaults to [])
```
[node]: https://nodejs.org/en/
......@@ -19,23 +19,30 @@ const config = {
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) {
Util.loadFiles(screenPath).then((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}`);
});
})
.then((renderers) => {
tomatoSauce.on('error', function (error) {
console.error(error.stack || error.message || error);
process.exit(1);
});
config.renderers = renderers;
})
.then(() => {
const tomatoSauce = new TomatoSauce(config);
tomatoSauce.on('listening', (event) => {
tomatoSauce.run();
});
const address = event.data.server.address();
console.log(`Tomato Sauce listening on: ${address.address}:${address.port}`);
});
tomatoSauce.on('error', (error) => {
console.error(error.stack || error.message || error);
process.exit(1);
});
tomatoSauce.run();
});
## Classes
<dl>
<dt><a href="#TomatoSauce">TomatoSauce</a><code>EventEmitter</code></dt>
<dd></dd>
</dl>
## Members
<dl>
<dt><a href="#Util">Util</a> : <code>Object</code></dt>
<dd><p>Module containing utility functions</p>
</dd>
</dl>
## Functions
<dl>
<dt><a href="#256ColorsRenderer">256ColorsRenderer()</a></dt>
<dd><p>Returns a 256 color, see <a href="https://en.wikipedia.org/wiki/ANSI_escape_code#Colors">https://en.wikipedia.org/wiki/ANSI_escape_code#Colors</a>
for more info.</p>
</dd>
<dt><a href="#ANSIRenderer">ANSIRenderer()</a></dt>
<dd><p>Returns a basic ansi color, see <a href="https://en.wikipedia.org/wiki/ANSI_escape_code#Colors">https://en.wikipedia.org/wiki/ANSI_escape_code#Colors</a>
for more info.</p>
</dd>
<dt><a href="#FakeColorRenderer">FakeColorRenderer()</a></dt>
<dd><p>Returns a malformed 24-bit ansi color</p>
</dd>
<dt><a href="#TrueColorRenderer">TrueColorRenderer()</a></dt>
<dd><p>Returns an ANSI code for 24-bit True Color, see
<a href="https://en.wikipedia.org/wiki/ANSI_escape_code#Colors">https://en.wikipedia.org/wiki/ANSI_escape_code#Colors</a> and look for
24-bit colors for more info. Only looks good in supported terminals,
otherwise looks like FakeColor</p>
</dd>
<dt><a href="#CircleScreen">CircleScreen()</a></dt>
<dd><p>Draws concentric circles. Each ring has its own color.</p>
</dd>
<dt><a href="#GradientsScreen">GradientsScreen()</a></dt>
<dd><p>Draws moving gradient boxes</p>
</dd>
<dt><a href="#MirrorsScreen">MirrorsScreen()</a></dt>
<dd><p>Draws small moving gradient boxes and repeats them.</p>
</dd>
<dt><a href="#RandomScreen">RandomScreen()</a></dt>
<dd><p>Draws random colors</p>
</dd>
<dt><a href="#SprinklesScreen">SprinklesScreen()</a></dt>
<dd><p>Draws random sprinkles in the screen each frame. Same color per
frame.</p>
</dd>
</dl>
## Interfaces
<dl>
<dt><a href="#IScreen">IScreen</a><code>String</code></dt>
<dd><p>A function that represents a screen, it is called frequently and should
return a string consisting of commands to run.</p>
</dd>
<dt><a href="#IRenderer">IRenderer</a><code>String</code></dt>
<dd><p>A function that represents a renderer, it should take in a color in RGB and
return a string with the appropriate code to colorize the terminal.</p>
</dd>
</dl>
<a name="IScreen"></a>
## IScreen ⇒ <code>String</code>
A function that represents a screen, it is called frequently and should
return a string consisting of commands to run.
**Kind**: global interface
**Returns**: <code>String</code> - The commands used to render the screen elements
| Param | Type | Description |
| --- | --- | --- |
| modulation | <code>Number</code> | A number between 0 and 255 representing the current step of the modulation |
| width | <code>Number</code> | The width of the screen |
| height | <code>Number</code> | The height of the screen |
| renderer | [<code>IRenderer</code>](#IRenderer) | The renderer used to colorize the scfeen |
<a name="IRenderer"></a>
## IRenderer ⇒ <code>String</code>
A function that represents a renderer, it should take in a color in RGB and
return a string with the appropriate code to colorize the terminal.
**Kind**: global interface
**Returns**: <code>String</code> - The commands used to colorize the terminal
| Param | Type | Description |
| --- | --- | --- |
| red | <code>Number</code> | The red component of the color between 0 and 255 |
| green | <code>Number</code> | The green component of the color between 0 and 255 |
| blue | <code>Number</code> | The green component of the color between 0 and 255 |
<a name="TomatoSauce"></a>
## TomatoSauce ⇐ <code>EventEmitter</code>
**Kind**: global class
**Extends**: <code>EventEmitter</code>
**Properties**
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| screens | [<code>Array.&lt;IScreen&gt;</code>](#IScreen) | | an array of screens available to serve |
| renderers | [<code>Array.&lt;IRenderer&gt;</code>](#IRenderer) | | an array of renderers available to colorize |
| [port] | <code>Number</code> | <code>9999</code> | the port to listen on |
| [frequency] | <code>Number</code> | <code>333</code> | how often to update the screen |
| [modulation] | <code>Number</code> | <code>5</code> | number between 0-255 depicting current modulation step |
* [TomatoSauce](#TomatoSauce)<code>EventEmitter</code>
* [new TomatoSauce(config)](#new_TomatoSauce_new)
* [.run()](#TomatoSauce+run)
<a name="new_TomatoSauce_new"></a>
### new TomatoSauce(config)
The main application for tomato sauce. Listens for connections and serves
random combinations of screens and renderers
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.
| Param | Type | Description |
| --- | --- | --- |
| config | <code>object</code> | the configuration object used to extend the properties. |
<a name="TomatoSauce+run"></a>
### tomatoSauce.run()
Main entry point, initializes the server and binds events for connections
**Kind**: instance method of [<code>TomatoSauce</code>](#TomatoSauce)
<a name="Util"></a>
## Util : <code>Object</code>
Module containing utility functions
**Kind**: global variable
* [Util](#Util) : <code>Object</code>
* [.parse16BitBuffer(buffer)](#Util.parse16BitBuffer)<code>Number</code>
* [.pickRandom(array)](#Util.pickRandom)<code>Any</code>
* [.loadFiles(path)](#Util.loadFiles)<code>Array</code>
<a name="Util.parse16BitBuffer"></a>
### Util.parse16BitBuffer(buffer) ⇒ <code>Number</code>
Parses a 16 bit number buffer
**Kind**: static method of [<code>Util</code>](#Util)
**Returns**: <code>Number</code> - the parsed value
| Param | Type | Description |
| --- | --- | --- |
| buffer | <code>Array.&lt;String&gt;</code> | the buffer to parse |
<a name="Util.pickRandom"></a>
### Util.pickRandom(array) ⇒ <code>Any</code>
Picks a random element from an array
**Kind**: static method of [<code>Util</code>](#Util)
**Returns**: <code>Any</code> - the picked element
| Param | Type | Description |
| --- | --- | --- |
| array | <code>Array</code> | the array to use |
<a name="Util.loadFiles"></a>
### Util.loadFiles(path) ⇒ <code>Array</code>
For a gi ven path, requires all of the files and returns an array
with the results. If the directory contains any non-requireable
files, it will fail.
**Kind**: static method of [<code>Util</code>](#Util)
**Returns**: <code>Array</code> - the array of all the loaded modules
| Param | Type | Description |
| --- | --- | --- |
| path | <code>String</code> | the path where the files are located |
<a name="256ColorsRenderer"></a>
## 256ColorsRenderer()
Returns a 256 color, see https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
for more info.
**Kind**: global function
**Implements**: [<code>IRenderer</code>](#IRenderer)
<a name="ANSIRenderer"></a>
## ANSIRenderer()
Returns a basic ansi color, see https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
for more info.
**Kind**: global function
**Implements**: [<code>IRenderer</code>](#IRenderer)
<a name="FakeColorRenderer"></a>
## FakeColorRenderer()
Returns a malformed 24-bit ansi color
**Kind**: global function
**Implements**: [<code>IRenderer</code>](#IRenderer)
<a name="TrueColorRenderer"></a>
## TrueColorRenderer()
Returns an ANSI code for 24-bit True Color, see
https://en.wikipedia.org/wiki/ANSI_escape_code#Colors and look for
24-bit colors for more info. Only looks good in supported terminals,
otherwise looks like FakeColor
**Kind**: global function
**Implements**: [<code>IRenderer</code>](#IRenderer)
<a name="CircleScreen"></a>
## CircleScreen()
Draws concentric circles. Each ring has its own color.
**Kind**: global function
**Implements**: [<code>IScreen</code>](#IScreen)
<a name="GradientsScreen"></a>
## GradientsScreen()
Draws moving gradient boxes
**Kind**: global function
**Implements**: [<code>IScreen</code>](#IScreen)
<a name="MirrorsScreen"></a>
## MirrorsScreen()
Draws small moving gradient boxes and repeats them.
**Kind**: global function
**Implements**: [<code>IScreen</code>](#IScreen)
<a name="RandomScreen"></a>
## RandomScreen()
Draws random colors
**Kind**: global function
**Implements**: [<code>IScreen</code>](#IScreen)
<a name="SprinklesScreen"></a>
## SprinklesScreen()
Draws random sprinkles in the screen each frame. Same color per
frame.
**Kind**: global function
**Implements**: [<code>IScreen</code>](#IScreen)
'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);
/**
* Returns a 256 color, see https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
* for more info.
*
* @function 256ColorsRenderer
* @implements IRenderer
*/
module.exports = function (red, blue, green) {
let colorNumber = 16 + 36 * redValue + 6 * greenValue + blueValue;
const redValue = Math.round(red * 5 / 255);
const blueValue = Math.round(blue * 5 / 255);
const greenValue = Math.round(green * 5 / 255);
const colorNumber = 16 + 36 * redValue + 6 * greenValue + blueValue;
return `\x1B[48;5;${colorNumber}m`;
};
module.exports = TwoFiftySixColors;
'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));
/**
* Returns a basic ansi color, see https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
* for more info.
*
* @function ANSIRenderer
* @implements IRenderer
*/
module.exports = function (red, blue, green) {
let colorNumber = 40 + colorOffset;
const colorOffset = Math.round((red + blue + green) * 7 / (255 * 3));
const colorNumber = 40 + colorOffset;
return `\x1B[${colorNumber}m`;
};
module.exports = ANSI;
'use strict';
// Sends malformed ANSI 24-bit color strings
const FakeColor = function (red, blue, green) {
/**
* Returns a malformed 24-bit ansi color
*
* @function FakeColorRenderer
* @implements IRenderer
*/
module.exports = function (red, blue, green) {
return `\x1B[28;2;${Math.round(red)};${Math.round(green)};${Math.round(blue)}m`;
};
module.exports = FakeColor;
......@@ -4,8 +4,16 @@
// 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) {
/**
* Returns an ANSI code for 24-bit True Color, see
* https://en.wikipedia.org/wiki/ANSI_escape_code#Colors and look for
* 24-bit colors for more info. Only looks good in supported terminals,
* otherwise looks like FakeColor
*
* @function TrueColorRenderer
* @implements IRenderer
*/
module.exports = function (red, blue, green) {
return `\x1B[48;2;${Math.round(red)};${Math.round(green)};${Math.round(blue)}m`;
};
module.exports = TrueColor;
'use strict';
// Draws concentric circles. Each ring has its own color.
const Circle = function (modulation, width, height, renderer) {
let response = [];
/**
* Draws concentric circles. Each ring has its own color.
*
* @function CircleScreen
* @implements IScreen
*/
module.exports = function (modulation, width, height, renderer) {
let circles = width > height ? height : width;
const response = [];
for (let i = 0; i < circles; i++) {
let centerX = Math.round(width / 2) + 1;
let centerY = Math.round(height / 2) + 1;
const circles = width > height ? height : width;
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 < circles; ++i) {
const centerX = Math.round(width / 2) + 1;
const centerY = Math.round(height / 2) + 1;
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);
const red = Math.floor(Math.random() * 255);
const blue = Math.floor(Math.random() * 255);
const green = Math.floor(Math.random() * 255);
for (let j = 0; j < 180; ++j) {
const angle = 2 * j * (Math.PI / 180);
const x = Math.round(centerX + Math.sin(angle) * i);
const 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)
const position = `\x1B[${y};${x}H`; // Move cursor to y,x (CSI y;x H)
response += `${position}${renderer(red, blue, green)} `;
}
}
......@@ -28,5 +34,3 @@ const Circle = function (modulation, width, height, renderer) {
return response;
};
module.exports = Circle;
'use strict';
// Draws moving gradient boxes.
const Gradients = function (modulation, width, height, renderer) {
/**
* Draws moving gradient boxes
*
* @function GradientsScreen
* @implements IScreen
*/
module.exports = 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;
for (let i = 0; i < height; ++i) {
for (let j = 0; j < width; ++j) {
const red = ((modulation + i) * 255 / height) % 255;
const blue = ((modulation + j) * 255 / width) % 255;
const green = ((modulation + i * j) * 255 / (width * height)) % 255;
response = response + renderer(red, blue, green);
response = response + ' ';
......@@ -21,5 +27,3 @@ const Gradients = function (modulation, width, height, renderer) {
return response;
};
module.exports = Gradients;
'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), ' '];
/**
* Draws small moving gradient boxes and repeats them.
*
* @function MirrorsScreen
* @implements IScreen
*/
module.exports = function (modulation, width, height, renderer) {
const response = [];
const scale = 2 + Math.round(Math.random() * 4);
const scaledHeight = Math.floor(height / scale);
const scaledWidth = Math.floor(width / scale);
for (let i = 0; i < scaledHeight; ++i) {
const row = [];
for (let j = 0; j < scaledWidth; ++j) {
const red = ((modulation + i) * 255 / height) % 255;
const blue = ((modulation + j) * 255 / width) % 255;
const green = ((modulation + i * j) * 255 / (width * height)) % 255;
const cell = [renderer(red, blue, green), ' '];
row.push(cell.join(''));
}
let rowText = '';
for (let j = 0; j < scale; j++) {
for (let j = 0; j < scale; ++j) {
rowText += row.reverse().join('');
}
for (let j = 1; j < scale; j++) {
for (let j = 1; j < scale; ++j) {
response[j * i] = rowText;
response[(height - 1 - (j * i))] = rowText;
}
......@@ -32,5 +38,3 @@ const Mirrors = function (modulation, width, height, renderer) {
return response.join('\n');
};
module.exports = Mirrors;
'use strict';
// random colors
const Random = function (modulation, width, height, renderer) {
/**
* Draws random colors
*
* @function RandomScreen
* @implements IScreen
*/
module.exports = 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);
for (let i = 0; i < height; ++i) {
for (let j = 0; j < width; ++j) {
const red = Math.floor(Math.random() * 255);
const blue = Math.floor(Math.random() * 255);
const green = Math.floor(Math.random() * 255);
response = response + renderer(red, blue, green);
response = response + ' ';
......@@ -21,5 +27,3 @@ const Random = function (modulation, width, height, renderer) {
return response;
};
module.exports = Random;
'use strict';
// Places random sprinkles in the screen each frame. Same color per
// frame.
const Sprinkles = function (modulation, width, height, renderer) {
/**
* Draws random sprinkles in the screen each frame. Same color per
* frame.
*
* @function SprinklesScreen
* @implements IScreen
*/
module.exports = 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;
const maxSprinkleCount = (width * height) / 2;
const minSprinkleCount = (width * height) / 8;
const 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);
const red = Math.floor(Math.random() * 255);
const blue = Math.floor(Math.random() * 255);
const 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;
for (let i = 0; i < sprinkleCount; ++i) {
const x = Math.round(Math.random() * (width - 1)) + 1;
const y = Math.round(Math.random() * (height - 1)) + 1;
let position = `\x1B[${y};${x}H`; // Move cursor to y,x (CSI y;x H)
const 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;
......@@ -5,89 +5,150 @@ 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 internals = {
// Interpret as Command Sequences.
kEscape: new Buffer([0xFF, 0xF4, 0XFF, 0xFD, 0x06]), // IAC IP IAC DO TIMING_MARK
kNAWSRequest: new Buffer([0xFF, 0xFD, 0X1F]), // IAC DO NAWS
kNAWSResponse: new Buffer([0xFF, 0xFB, 0X1F, 0xFF, 0xFA, 0X1F]) // IAC WILL NAWS IAC SB NAWS
};
/**
* A function that represents a screen, it is called frequently and should
* return a string consisting of commands to run.
*
* @interface IScreen
* @type function
* @param {Number} modulation A number between 0 and 255 representing the current
* step of the modulation
* @param {Number} width The width of the screen
* @param {Number} height The height of the screen
* @param {IRenderer} renderer The renderer used to colorize the scfeen
* @return {String} The commands used to render the screen elements
*/
/**
* A function that represents a renderer, it should take in a color in RGB and
* return a string with the appropriate code to colorize the terminal.
*
* @interface IRenderer
* @type function
* @param {Number} red The red component of the color between 0 and 255
* @param {Number} green The green component of the color between 0 and 255
* @param {Number} blue The green component of the color between 0 and 255
* @return {String} The commands used to colorize the terminal
*/
/**
* The main application for tomato sauce. Listens for connections and serves
* random combinations of screens and renderers
*
* 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.
*
* @class TomatoSauce
* @extends EventEmitter
*
* @param {object} config the configuration object used to extend the properties.
*
* @property {Array<IScreen>} screens an array of screens available to serve
* @property {Array<IRenderer>} renderers an array of renderers available to colorize
* @property {Number} [port=9999] the port to listen on
* @property {Number} [frequency=333] how often to update the screen
* @property {Number} [modulation=5] number between 0-255 depicting current modulation step
*/
const TomatoSauce = class TomatoSauce extends EventEmitter {
constructor (config) {
constructor(config = {}) {
super();
this.screens = [];
this.renderers = [];
Object.assign(this, config || {});
// Defaults.
this.port = 9999;
this.frequency = 333;
this.modulation = 5;
Object.assign(this, config);
}
// Here's where things get started.
run () {
/**
* Main entry point, initializes the server and binds events for connections
*
* @function run
* @instance
* @memberof TomatoSauce
*/
run() {
this._startServer();
this._bindEvents();
}
// Creates a socket, server based on the configuration. Emits the
// listening event when ready.
_startServer () {
_startServer() {
const server = Net.createServer();
this.server = server;
server.listen(this.port, function () {
server.listen(this.port, () => {
this.emit('listening', {
data: {
server: server
server
}
});
}.bind(this));
});
}
_bindEvents () {
// Binds the connection and error events
_bindEvents() {
// Send the error event all the way up.
this.server.on('error', function (err) {
this.server.on('error', (error) => {
this.emit('error', {
data: {
error: err
error
}
});
}.bind(this));
});
// Send the error event all the way up.
this.server.on('connection', function (socket) {
this.server.on('connection', (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 = {
_renderScreen(socket) {
const connectionData = {
width: null,
height: null,
modulation: 0,
screen: this._getScreen(),
renderer: this._getRenderer(),
socket: socket
socket
};
let interval = null;
const interval = null;
socket.write(internals.kNAWSRequest);
socket.write(kNAWSRequest);
socket.on('data', (data) => {
socket.on('data', function (data) {
if (data.slice(0, 6).compare(kNAWSResponse) === 0) {
if (data.slice(0, 6).compare(internals.kNAWSResponse) === 0) {
connectionData.width = Util.parse16BitBuffer(data.slice(6, 8));
connectionData.height = Util.parse16BitBuffer(data.slice(8, 10));
......@@ -95,35 +156,34 @@ const TomatoSauce = class TomatoSauce extends EventEmitter {
interval = setInterval(this._writeMessage.bind(this, connectionData), this.frequency);
}
if (data.compare(kEscape) === 0) {
if (data.compare(internals.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
_writeMessage(connectionData) {
const payload = connectionData.screen(connectionData.modulation, connectionData.width, connectionData.height, connectionData.renderer);
const message = `\x1B[1;1H${payload}`; // reset cursor position before payload
connectionData.modulation = (connectionData.modulation + this.modulation) % 256;
connectionData.socket.write(message);
}
_getScreen () {
_getScreen() {
return Util.pickRandom(this.screens);
}
_getRenderer () {
_getRenderer() {
return Util.pickRandom(this.renderers);
}
};
// Defaults.
TomatoSauce.prototype.port = 9999;
TomatoSauce.prototype.frequency = 333;
TomatoSauce.prototype.modulation = 5;
module.exports = TomatoSauce;
......@@ -3,38 +3,70 @@
const Fs = require('fs');
const Path = require('path');
// Module containing utility functions.
/**
* Module containing utility functions
*
* @name Util
* @type Object
*/
const Util = {
// Parses a 16 bit number buffer.
parse16BitBuffer: function (buffer) {
/**
* Parses a 16 bit number buffer
*
* @function parse16BitBuffer
* @memberof Util
* @param {Array<String>} buffer the buffer to parse
* @return {Number} the parsed value
*/
parse16BitBuffer(buffer) {
return buffer[0] * 256 + buffer[1];
},
// Picks a random element from an array.
pickRandom: function (array) {
/**
* Picks a random element from an array
*
* @function pickRandom
* @memberof Util
* @param {Array} array the array to use
* @return {Any} the picked element
*/
pickRandom(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) {
/**
* For a gi ven path, requires all of the files and returns an array
* with the results. If the directory contains any non-requireable
* files, it will fail.
*
* @function loadFiles
* @memberof Util
* @param {String} path the path where the files are located
* @return {Array} the array of all the loaded modules
*/
loadFiles(path) {
return new Promise((resolve, reject) => {
Fs.readdir(path, (err, files) => {
if (err) {
return reject(err);
}
let loadedFiles = [];
const loadedFiles = [];
for (let file of files) {
let filePath = Path.join(path, file);
for (const file of files) {
const filePath = Path.join(path, file);
let loadedFile;
try {
loadedFile = require(filePath);
} catch (err) {
}
catch (err) {
return reject(err);
}
......
This diff is collapsed.
{
"name": "tomato-sauce",
"version": "1.0.0",
"description": "\"Telnet based drawing functions and renderers\"",
"description": "Telnet based drawing functions and renderers",
"main": "lib/tomato_sauce.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"document": "jsdoc2md -f 'lib/**/*.js' > doc/README.md",
"lint": "eslint lib bin",
"test": "I'm really really sorry! && exit 1",
"start": "bin/tomato_sauce.js"
},
"repository": {
"type": "git",
......@@ -25,5 +28,12 @@
"homepage": "https://github.com/rbdr/tomato-sauce#readme",
"dependencies": {
"getenv": "^0.6.0"
},
"devDependencies": {
"eslint": "^4.19.1",
"eslint-config-hapi": "^11.0.0",
"eslint-plugin-hapi": "^4.1.0",
"jsdoc": "^3.5.5",
"jsdoc-to-markdown": "^4.0.1"
}
}