package/package.json000644 001750 001750 0000001262 13020637002013011 0ustar00000000 000000 { "name": "charm", "version": "1.0.2", "description": "ansi control sequences for terminal cursor hopping and colors", "main": "index.js", "directories": { "lib": ".", "example": "example", "test": "test" }, "repository": { "type": "git", "url": "http://github.com/substack/node-charm.git" }, "keywords": [ "terminal", "ansi", "cursor", "color", "console", "control", "escape", "sequence" ], "dependencies": { "inherits": "^2.0.1" }, "author": { "name": "James Halliday", "email": "mail@substack.net", "url": "http://substack.net" }, "license": "MIT", "engine": { "node": ">=0.4" } } package/index.js000644 001750 001750 0000020304 13020636766012206 0ustar00000000 000000 var tty = require('tty'); var encode = require('./lib/encode'); var Stream = require('stream').Stream; var inherits = require('inherits') inherits(Charm, Stream) var exports = module.exports = function () { var input = null; function setInput (s) { if (input) throw new Error('multiple inputs specified') else input = s } var output = null; function setOutput (s) { if (output) throw new Error('multiple outputs specified') else output = s } for (var i = 0; i < arguments.length; i++) { var arg = arguments[i]; if (!arg) continue; if (arg.readable) setInput(arg) else if (arg.stdin || arg.input) setInput(arg.stdin || arg.input) if (arg.writable) setOutput(arg) else if (arg.stdout || arg.output) setOutput(arg.stdout || arg.output) } if (input && typeof input.fd === 'number' && tty.isatty(input.fd)) { if (process.stdin.setRawMode) { process.stdin.setRawMode(true); } else tty.setRawMode(true); } var charm = new Charm; if (input) { input.pipe(charm); } if (output) { charm.pipe(output); } charm.once('^C', process.exit); charm.once('end', function () { if (input) { if (typeof input.fd === 'number' && tty.isatty(input.fd)) { if (process.stdin.setRawMode) { process.stdin.setRawMode(false); } else tty.setRawMode(false); } input.destroy(); } }); return charm; }; function Charm () { this.writable = true; this.readable = true; this.pending = []; } exports.Charm = Charm Charm.prototype.write = function (buf) { var self = this; if (self.pending.length) { var codes = extractCodes(buf); var matched = false; for (var i = 0; i < codes.length; i++) { for (var j = 0; j < self.pending.length; j++) { var cb = self.pending[j]; if (cb(codes[i])) { matched = true; self.pending.splice(j, 1); break; } } } if (matched) return; } if (buf.length === 1) { if (buf[0] === 3) self.emit('^C'); if (buf[0] === 4) self.emit('^D'); } self.emit('data', buf); return self; }; Charm.prototype.destroy = function () { this.end(); }; Charm.prototype.end = function (buf) { if (buf) this.write(buf); this.emit('end'); }; Charm.prototype.reset = function (cb) { // resets the screen on iTerm, which appears // to lack support for the reset character. this.write(encode('[0m')); this.write(encode('[2J')); this.write(encode('c')); return this; }; Charm.prototype.position = function (x, y) { // get/set absolute coordinates if (typeof x === 'function') { var cb = x; this.pending.push(function (buf) { if (buf[0] === 27 && buf[1] === encode.ord('[') && buf[buf.length-1] === encode.ord('R')) { var pos = buf.toString() .slice(2,-1) .split(';') .map(Number) ; cb(pos[1], pos[0]); return true; } }); this.write(encode('[6n')); } else { this.write(encode( '[' + Math.floor(y) + ';' + Math.floor(x) + 'f' )); } return this; }; Charm.prototype.move = function (x, y) { // set relative coordinates var bufs = []; if (y < 0) this.up(-y) else if (y > 0) this.down(y) if (x > 0) this.right(x) else if (x < 0) this.left(-x) return this; }; Charm.prototype.up = function (y) { if (y === undefined) y = 1; this.write(encode('[' + Math.floor(y) + 'A')); return this; }; Charm.prototype.down = function (y) { if (y === undefined) y = 1; this.write(encode('[' + Math.floor(y) + 'B')); return this; }; Charm.prototype.right = function (x) { if (x === undefined) x = 1; this.write(encode('[' + Math.floor(x) + 'C')); return this; }; Charm.prototype.left = function (x) { if (x === undefined) x = 1; this.write(encode('[' + Math.floor(x) + 'D')); return this; }; Charm.prototype.column = function (x) { this.write(encode('[' + Math.floor(x) + 'G')); return this; }; Charm.prototype.push = function (withAttributes) { this.write(encode(withAttributes ? '7' : '[s')); return this; }; Charm.prototype.pop = function (withAttributes) { this.write(encode(withAttributes ? '8' : '[u')); return this; }; Charm.prototype.erase = function (s) { if (s === 'end' || s === '$') { this.write(encode('[K')); } else if (s === 'start' || s === '^') { this.write(encode('[1K')); } else if (s === 'line') { this.write(encode('[2K')); } else if (s === 'down') { this.write(encode('[J')); } else if (s === 'up') { this.write(encode('[1J')); } else if (s === 'screen') { this.write(encode('[1J')); } else { this.emit('error', new Error('Unknown erase type: ' + s)); } return this; }; Charm.prototype.delete = function (s, n) { n = n || 1 if (s === 'line') { this.write(encode('[' + n + 'M')); } else if (s === 'char') { this.write(encode('[' + n + 'M')); } else { this.emit('error', new Error('Unknown delete type: ' + s)); } return this; }; Charm.prototype.insert = function (mode, n) { n = n || 1 if(mode === true) { this.write(encode('[4h')); } else if (mode === false) { this.write(encode('[l')); } else if (mode === 'line') { this.write(encode('[' + n + 'L')); } else if (mode === 'char') { this.write(encode('[' + n + '@')); } else { this.emit('error', new Error('Unknown delete type: ' + s)); } return this; }; Charm.prototype.display = function (attr) { var c = { reset : 0, bright : 1, dim : 2, underscore : 4, blink : 5, reverse : 7, hidden : 8 }[attr]; if (c === undefined) { this.emit('error', new Error('Unknown attribute: ' + attr)); } this.write(encode('[' + c + 'm')); return this; }; Charm.prototype.foreground = function (color) { if (typeof color === 'number') { if (color < 0 || color >= 256) { this.emit('error', new Error('Color out of range: ' + color)); } this.write(encode('[38;5;' + color + 'm')); } else { var c = { black : 30, red : 31, green : 32, yellow : 33, blue : 34, magenta : 35, cyan : 36, white : 37 }[color.toLowerCase()]; if (!c) this.emit('error', new Error('Unknown color: ' + color)); this.write(encode('[' + c + 'm')); } return this; }; Charm.prototype.background = function (color) { if (typeof color === 'number') { if (color < 0 || color >= 256) { this.emit('error', new Error('Color out of range: ' + color)); } this.write(encode('[48;5;' + color + 'm')); } else { var c = { black : 40, red : 41, green : 42, yellow : 43, blue : 44, magenta : 45, cyan : 46, white : 47 }[color.toLowerCase()]; if (!c) this.emit('error', new Error('Unknown color: ' + color)); this.write(encode('[' + c + 'm')); } return this; }; Charm.prototype.cursor = function (visible) { this.write(encode(visible ? '[?25h' : '[?25l')); return this; }; var extractCodes = exports.extractCodes = function (buf) { var codes = []; var start = -1; for (var i = 0; i < buf.length; i++) { if (buf[i] === 27) { if (start >= 0) codes.push(buf.slice(start, i)); start = i; } else if (start >= 0 && i === buf.length - 1) { codes.push(buf.slice(start)); } } return codes; } package/README.markdown000644 001750 001750 0000011171 13020636766013244 0ustar00000000 000000 charm ===== Use [ansi terminal characters](http://www.termsys.demon.co.uk/vtansi.htm) to write colors and cursor positions. ![me lucky charms](http://substack.net/images/charms.png) example ======= lucky ----- ````javascript var charm = require('charm')(); charm.pipe(process.stdout); charm.reset(); var colors = [ 'red', 'cyan', 'yellow', 'green', 'blue' ]; var text = 'Always after me lucky charms.'; var offset = 0; var iv = setInterval(function () { var y = 0, dy = 1; for (var i = 0; i < 40; i++) { var color = colors[(i + offset) % colors.length]; var c = text[(i + offset) % text.length]; charm .move(1, dy) .foreground(color) .write(c) ; y += dy; if (y <= 0 || y >= 5) dy *= -1; } charm.position(0, 1); offset ++; }, 150); ```` events ====== Charm objects pass along the data events from their input stream except for events generated from querying the terminal device. Because charm puts stdin into raw mode, charm emits two special events: "^C" and "^D" when the user types those combos. It's super convenient with these events to do: ````javascript charm.on('^C', process.exit) ```` The above is set on all `charm` streams. If you want to add your own handling for these special events simply: ````javascript charm.removeAllListeners('^C') charm.on('^C', function () { // Don't exit. Do some mad science instead. }) ```` methods ======= var charm = require('charm')(param or stream, ...) -------------------------------------------------- Create a new readable/writable `charm` stream. You can pass in readable or writable streams as parameters and they will be piped to or from accordingly. You can also pass `process` in which case `process.stdin` and `process.stdout` will be used. You can `pipe()` to and from the `charm` object you get back. charm.reset() ------------- Reset the entire screen, like the /usr/bin/reset command. charm.destroy(), charm.end() ---------------------------- Emit an `"end"` event downstream. charm.write(msg) ---------------- Pass along `msg` to the output stream. charm.position(x, y) -------------------- Set the cursor position to the absolute coordinates `x, y`. charm.position(cb) ------------------ Query the absolute cursor position from the input stream through the output stream (the shell does this automatically) and get the response back as `cb(x, y)`. charm.move(x, y) ---------------- Move the cursor position by the relative coordinates `x, y`. charm.up(y) ----------- Move the cursor up by `y` rows. charm.down(y) ------------- Move the cursor down by `y` rows. charm.left(x) ------------- Move the cursor left by `x` columns. charm.right(x) -------------- Move the cursor right by `x` columns. charm.push(withAttributes=false) -------------------------------- Push the cursor state and optionally the attribute state. charm.pop(withAttributes=false) ------------------------------- Pop the cursor state and optionally the attribute state. charm.erase(s) -------------- Erase a region defined by the string `s`. `s` can be: * end - erase from the cursor to the end of the line * start - erase from the cursor to the start of the line * line - erase the current line * down - erase everything below the current line * up - erase everything above the current line * screen - erase the entire screen charm.delete(mode, n) --------------------- Delete `'line'` or `'char'`s. `delete` differs from erase because it does not write over the deleted characters with whitesapce, but instead removes the deleted space. `mode` can be `'line'` or `'char'`. `n` is the number of items to be deleted. `n` must be a positive integer. The cursor position is not updated. charm.insert(mode, n) --------------------- Insert space into the terminal. `insert` is the opposite of` delete`, and the arguments are the same. charm.display(attr) ------------------- Set the display mode with the string `attr`. `attr` can be: * reset * bright * dim * underscore * blink * reverse * hidden charm.foreground(color) ----------------------- Set the foreground color with the string `color`, which can be: * red * yellow * green * blue * cyan * magenta * black * white or `color` can be an integer from 0 to 255, inclusive. charm.background(color) ----------------------- Set the background color with the string `color`, which can be: * red * yellow * green * blue * cyan * magenta * black * white or `color` can be an integer from 0 to 255, inclusive. charm.cursor(visible) --------------------- Set the cursor visibility with a boolean `visible`. install ======= With [npm](http://npmjs.org) do: ``` npm install charm ``` package/example/256.js000644 001750 001750 0000000502 13020636766013044 0ustar00000000 000000 var charm = require('../')(process); function exit () { charm.display('reset'); process.exit(); } charm.on('^C', exit); var ix = 0; var iv = setInterval(function () { charm.background(ix++).write(' '); if (ix === 256) { clearInterval(iv); charm.write('\n'); exit(); } }, 10); package/example/column.js000644 001750 001750 0000000242 13020636766014026 0ustar00000000 000000 var charm = require('../')(); charm.pipe(process.stdout); charm .column(16) .write('beep') .down() .column(32) .write('boop\n') .end() ; package/example/cursor.js000644 001750 001750 0000001013 13020636766014043 0ustar00000000 000000 var charm = require('../')(process); charm.position(5, 10); charm.position(function (x, y) { console.dir([ x, y ]); charm.move(7,2); charm.push(); process.stdout.write('lul'); charm.left(3).up(1).foreground('magenta'); process.stdout.write('v'); charm.left(1).up(1).display('reset'); process.stdout.write('|'); charm.down(3); charm.pop().background('blue'); process.stdout.write('popped\npow'); charm.display('reset').erase('line'); charm.destroy(); }); package/example/http_spin.js000644 001750 001750 0000001732 13020636766014546 0ustar00000000 000000 var http = require('http'); var charmer = require('../'); http.createServer(function (req, res) { res.setHeader('content-type', 'text/ansi'); var charm = charmer(); charm.pipe(res); charm.reset(); var radius = 10; var theta = 0; var points = []; var iv = setInterval(function () { var x = 2 + (radius + Math.cos(theta) * radius) * 2; var y = 2 + radius + Math.sin(theta) * radius; points.unshift([ x, y ]); var colors = [ 'red', 'yellow', 'green', 'cyan', 'blue', 'magenta' ]; points.forEach(function (p, i) { charm.position(p[0], p[1]); var c = colors[Math.floor(i / 12)]; charm.background(c).write(' ') }); points = points.slice(0, 12 * colors.length - 1); theta += Math.PI / 40; }, 50); req.connection.on('end', function () { clearInterval(iv); charm.end(); }); }).listen(8081); package/example/lucky.js000644 001750 001750 0000001142 13020636766013660 0ustar00000000 000000 var charm = require('../')(); charm.pipe(process.stdout); charm.reset(); var colors = [ 'red', 'cyan', 'yellow', 'green', 'blue' ]; var text = 'Always after me lucky charms.'; var offset = 0; var iv = setInterval(function () { var y = 0, dy = 1; for (var i = 0; i < 40; i++) { var color = colors[(i + offset) % colors.length]; var c = text[(i + offset) % text.length]; charm .move(1, dy) .foreground(color) .write(c) ; y += dy; if (y <= 0 || y >= 5) dy *= -1; } charm.position(0, 1); offset ++; }, 150); package/example/position.js000644 001750 001750 0000000217 13020636766014377 0ustar00000000 000000 var charm = require('charm')(process); charm.on('^C', process.exit); charm.position(function (x, y) { console.log('(%d, %d)', x, y); }); package/example/progress.js000644 001750 001750 0000000523 13020636766014377 0ustar00000000 000000 var charm = require('../')(); charm.pipe(process.stdout); charm.write('Progress: 0 %'); var i = 0; var iv = setInterval(function () { charm.left(i.toString().length + 2); i ++; charm.write(i + ' %'); if (i === 100) { charm.end('\nDone!\n'); clearInterval(iv); } }, 25); charm.on('^C',process.exit); package/example/spin.js000644 001750 001750 0000001152 13020636766013503 0ustar00000000 000000 var charm = require('../')(process); charm.reset(); var radius = 10; var theta = 0; var points = []; var iv = setInterval(function () { var x = 2 + (radius + Math.cos(theta) * radius) * 2; var y = 2 + radius + Math.sin(theta) * radius; points.unshift([ x, y ]); var colors = [ 'red', 'yellow', 'green', 'cyan', 'blue', 'magenta' ]; points.forEach(function (p, i) { charm.position(p[0], p[1]); var c = colors[Math.floor(i / 12)]; charm.background(c).write(' ') }); points = points.slice(0, 12 * colors.length - 1); theta += Math.PI / 40; }, 50); package/lib/encode.js000644 001750 001750 0000000711 13020636766013102 0ustar00000000 000000 var encode = module.exports = function (xs) { function bytes (s) { if (typeof s === 'string') { return s.split('').map(ord); } else if (Array.isArray(s)) { return s.reduce(function (acc, c) { return acc.concat(bytes(c)); }, []); } } return new Buffer([ 0x1b ].concat(bytes(xs))); }; var ord = encode.ord = function ord (c) { return c.charCodeAt(0) };