index.js 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. var tty = require('tty');
  2. var encode = require('./lib/encode');
  3. var Stream = require('stream').Stream;
  4. var exports = module.exports = function () {
  5. var input = null;
  6. function setInput (s) {
  7. if (input) throw new Error('multiple inputs specified')
  8. else input = s
  9. }
  10. var output = null;
  11. function setOutput (s) {
  12. if (output) throw new Error('multiple outputs specified')
  13. else output = s
  14. }
  15. for (var i = 0; i < arguments.length; i++) {
  16. var arg = arguments[i];
  17. if (!arg) continue;
  18. if (arg.readable) setInput(arg)
  19. else if (arg.stdin || arg.input) setInput(arg.stdin || arg.input)
  20. if (arg.writable) setOutput(arg)
  21. else if (arg.stdout || arg.output) setOutput(arg.stdout || arg.output)
  22. }
  23. if (input && typeof input.fd === 'number' && tty.isatty(input.fd)) {
  24. if (process.stdin.setRawMode) {
  25. process.stdin.setRawMode(true);
  26. }
  27. else tty.setRawMode(true);
  28. }
  29. var charm = new Charm;
  30. if (input) {
  31. input.pipe(charm);
  32. }
  33. if (output) {
  34. charm.pipe(output);
  35. }
  36. charm.once('^C', process.exit);
  37. charm.once('end', function () {
  38. if (input) {
  39. if (typeof input.fd === 'number' && tty.isatty(input.fd)) {
  40. if (process.stdin.setRawMode) {
  41. process.stdin.setRawMode(false);
  42. }
  43. else tty.setRawMode(false);
  44. }
  45. input.destroy();
  46. }
  47. });
  48. return charm;
  49. };
  50. var Charm = exports.Charm = function Charm () {
  51. this.writable = true;
  52. this.readable = true;
  53. this.pending = [];
  54. }
  55. Charm.prototype = new Stream;
  56. Charm.prototype.write = function (buf) {
  57. var self = this;
  58. if (self.pending.length) {
  59. var codes = extractCodes(buf);
  60. var matched = false;
  61. for (var i = 0; i < codes.length; i++) {
  62. for (var j = 0; j < self.pending.length; j++) {
  63. var cb = self.pending[j];
  64. if (cb(codes[i])) {
  65. matched = true;
  66. self.pending.splice(j, 1);
  67. break;
  68. }
  69. }
  70. }
  71. if (matched) return;
  72. }
  73. if (buf.length === 1) {
  74. if (buf[0] === 3) self.emit('^C');
  75. if (buf[0] === 4) self.emit('^D');
  76. }
  77. self.emit('data', buf);
  78. return self;
  79. };
  80. Charm.prototype.destroy = function () {
  81. this.end();
  82. };
  83. Charm.prototype.end = function (buf) {
  84. if (buf) this.write(buf);
  85. this.emit('end');
  86. };
  87. Charm.prototype.reset = function (cb) {
  88. this.write(encode('c'));
  89. return this;
  90. };
  91. Charm.prototype.position = function (x, y) {
  92. // get/set absolute coordinates
  93. if (typeof x === 'function') {
  94. var cb = x;
  95. this.pending.push(function (buf) {
  96. if (buf[0] === 27 && buf[1] === encode.ord('[')
  97. && buf[buf.length-1] === encode.ord('R')) {
  98. var pos = buf.toString()
  99. .slice(2,-1)
  100. .split(';')
  101. .map(Number)
  102. ;
  103. cb(pos[1], pos[0]);
  104. return true;
  105. }
  106. });
  107. this.write(encode('[6n'));
  108. }
  109. else {
  110. this.write(encode(
  111. '[' + Math.floor(y) + ';' + Math.floor(x) + 'f'
  112. ));
  113. }
  114. return this;
  115. };
  116. Charm.prototype.move = function (x, y) {
  117. // set relative coordinates
  118. var bufs = [];
  119. if (y < 0) this.up(-y)
  120. else if (y > 0) this.down(y)
  121. if (x > 0) this.right(x)
  122. else if (x < 0) this.left(-x)
  123. return this;
  124. };
  125. Charm.prototype.up = function (y) {
  126. if (y === undefined) y = 1;
  127. this.write(encode('[' + Math.floor(y) + 'A'));
  128. return this;
  129. };
  130. Charm.prototype.down = function (y) {
  131. if (y === undefined) y = 1;
  132. this.write(encode('[' + Math.floor(y) + 'B'));
  133. return this;
  134. };
  135. Charm.prototype.right = function (x) {
  136. if (x === undefined) x = 1;
  137. this.write(encode('[' + Math.floor(x) + 'C'));
  138. return this;
  139. };
  140. Charm.prototype.left = function (x) {
  141. if (x === undefined) x = 1;
  142. this.write(encode('[' + Math.floor(x) + 'D'));
  143. return this;
  144. };
  145. Charm.prototype.column = function (x) {
  146. this.write(encode('[' + Math.floor(x) + 'G'));
  147. return this;
  148. };
  149. Charm.prototype.push = function (withAttributes) {
  150. this.write(encode(withAttributes ? '7' : '[s'));
  151. return this;
  152. };
  153. Charm.prototype.pop = function (withAttributes) {
  154. this.write(encode(withAttributes ? '8' : '[u'));
  155. return this;
  156. };
  157. Charm.prototype.erase = function (s) {
  158. if (s === 'end' || s === '$') {
  159. this.write(encode('[K'));
  160. }
  161. else if (s === 'start' || s === '^') {
  162. this.write(encode('[1K'));
  163. }
  164. else if (s === 'line') {
  165. this.write(encode('[2K'));
  166. }
  167. else if (s === 'down') {
  168. this.write(encode('[J'));
  169. }
  170. else if (s === 'up') {
  171. this.write(encode('[1J'));
  172. }
  173. else if (s === 'screen') {
  174. this.write(encode('[1J'));
  175. }
  176. else {
  177. this.emit('error', new Error('Unknown erase type: ' + s));
  178. }
  179. return this;
  180. };
  181. Charm.prototype.display = function (attr) {
  182. var c = {
  183. reset : 0,
  184. bright : 1,
  185. dim : 2,
  186. underscore : 4,
  187. blink : 5,
  188. reverse : 7,
  189. hidden : 8
  190. }[attr];
  191. if (c === undefined) {
  192. this.emit('error', new Error('Unknown attribute: ' + attr));
  193. }
  194. this.write(encode('[' + c + 'm'));
  195. return this;
  196. };
  197. Charm.prototype.foreground = function (color) {
  198. if (typeof color === 'number') {
  199. if (color < 0 || color >= 256) {
  200. this.emit('error', new Error('Color out of range: ' + color));
  201. }
  202. this.write(encode('[38;5;' + color + 'm'));
  203. }
  204. else {
  205. var c = {
  206. black : 30,
  207. red : 31,
  208. green : 32,
  209. yellow : 33,
  210. blue : 34,
  211. magenta : 35,
  212. cyan : 36,
  213. white : 37
  214. }[color.toLowerCase()];
  215. if (!c) this.emit('error', new Error('Unknown color: ' + color));
  216. this.write(encode('[' + c + 'm'));
  217. }
  218. return this;
  219. };
  220. Charm.prototype.background = function (color) {
  221. if (typeof color === 'number') {
  222. if (color < 0 || color >= 256) {
  223. this.emit('error', new Error('Color out of range: ' + color));
  224. }
  225. this.write(encode('[48;5;' + color + 'm'));
  226. }
  227. else {
  228. var c = {
  229. black : 40,
  230. red : 41,
  231. green : 42,
  232. yellow : 43,
  233. blue : 44,
  234. magenta : 45,
  235. cyan : 46,
  236. white : 47
  237. }[color.toLowerCase()];
  238. if (!c) this.emit('error', new Error('Unknown color: ' + color));
  239. this.write(encode('[' + c + 'm'));
  240. }
  241. return this;
  242. };
  243. Charm.prototype.cursor = function (visible) {
  244. this.write(encode(visible ? '[?25h' : '[?25l'));
  245. return this;
  246. };
  247. var extractCodes = exports.extractCodes = function (buf) {
  248. var codes = [];
  249. var start = -1;
  250. for (var i = 0; i < buf.length; i++) {
  251. if (buf[i] === 27) {
  252. if (start >= 0) codes.push(buf.slice(start, i));
  253. start = i;
  254. }
  255. else if (start >= 0 && i === buf.length - 1) {
  256. codes.push(buf.slice(start));
  257. }
  258. }
  259. return codes;
  260. }