index.js 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  1. /**
  2. * ansi-viewer
  3. * ANSI art viewer for node.
  4. * Copyright (c) 2015, Christopher Jeffrey and contributors (MIT License).
  5. * https://github.com/chjj/blessed
  6. */
  7. var blessed = require('blessed')
  8. , request = require('request')
  9. , singlebyte = require('./singlebyte')
  10. , fs = require('fs');
  11. // $ wget -r -o log --tries=10 'http://artscene.textfiles.com/ansi/'
  12. // $ grep 'http.*\.ans$' log | awk '{ print $3 }' > ansi-art.list
  13. var urls = fs.readFileSync(__dirname + '/ansi-art.list', 'utf8').trim().split('\n');
  14. var map = urls.reduce(function(map, url) {
  15. map[/([^.\/]+\/[^.\/]+)\.ans$/.exec(url)[1]] = url;
  16. return map;
  17. }, {});
  18. var max = Object.keys(map).reduce(function(out, text) {
  19. return Math.max(out, text.length);
  20. }, 0) + 6;
  21. var screen = blessed.screen({
  22. smartCSR: true,
  23. dockBorders: true
  24. });
  25. var art = blessed.terminal({
  26. parent: screen,
  27. left: 0,
  28. top: 0,
  29. height: 60,
  30. // some are 78/80, some are 80/82
  31. width: 82,
  32. border: 'line',
  33. tags: true,
  34. label: ' {bold}{cyan-fg}ANSI Art{/cyan-fg}{/bold} (Drag Me) ',
  35. handler: function() {},
  36. draggable: true
  37. });
  38. var list = blessed.list({
  39. parent: screen,
  40. label: ' {bold}{cyan-fg}Art List{/cyan-fg}{/bold} (Drag Me) ',
  41. tags: true,
  42. draggable: true,
  43. top: 0,
  44. right: 0,
  45. width: max,
  46. height: '50%',
  47. keys: true,
  48. vi: true,
  49. mouse: true,
  50. border: 'line',
  51. scrollbar: {
  52. ch: ' ',
  53. track: {
  54. bg: 'cyan'
  55. },
  56. style: {
  57. inverse: true
  58. }
  59. },
  60. style: {
  61. item: {
  62. hover: {
  63. bg: 'blue'
  64. }
  65. },
  66. selected: {
  67. bg: 'blue',
  68. bold: true
  69. }
  70. },
  71. search: function(callback) {
  72. prompt.input('Search:', '', function(err, value) {
  73. if (err) return;
  74. return callback(null, value);
  75. });
  76. }
  77. });
  78. var status = blessed.box({
  79. parent: screen,
  80. bottom: 0,
  81. right: 0,
  82. height: 1,
  83. width: 'shrink',
  84. style: {
  85. bg: 'blue'
  86. },
  87. content: 'Select your piece of ANSI art (`/` to search).'
  88. });
  89. var loader = blessed.loading({
  90. parent: screen,
  91. top: 'center',
  92. left: 'center',
  93. height: 5,
  94. align: 'center',
  95. width: '50%',
  96. tags: true,
  97. hidden: true,
  98. border: 'line'
  99. });
  100. var msg = blessed.message({
  101. parent: screen,
  102. top: 'center',
  103. left: 'center',
  104. height: 'shrink',
  105. width: '50%',
  106. align: 'center',
  107. tags: true,
  108. hidden: true,
  109. border: 'line'
  110. });
  111. var prompt = blessed.prompt({
  112. parent: screen,
  113. top: 'center',
  114. left: 'center',
  115. height: 'shrink',
  116. width: 'shrink',
  117. keys: true,
  118. vi: true,
  119. mouse: true,
  120. tags: true,
  121. border: 'line',
  122. hidden: true
  123. });
  124. list.setItems(Object.keys(map));
  125. list.on('select', function(el, selected) {
  126. if (list._.rendering) return;
  127. var name = el.getText();
  128. var url = map[name];
  129. status.setContent(url);
  130. list._.rendering = true;
  131. loader.load('Loading...');
  132. request({
  133. uri: url,
  134. encoding: null
  135. }, function(err, res, body) {
  136. list._.rendering = false;
  137. loader.stop();
  138. if (err) {
  139. return msg.error(err.message);
  140. }
  141. if (!body) {
  142. return msg.error('No body.');
  143. }
  144. return cp437ToUtf8(body, function(err, body) {
  145. if (err) {
  146. return msg.error(err.message);
  147. }
  148. if (process.argv[2] === '--debug') {
  149. var filename = name.replace(/\//g, '.') + '.ans';
  150. fs.writeFileSync(__dirname + '/' + filename, body);
  151. }
  152. // Remove text:
  153. body = body.replace('Downloaded From P-80 International Information Systems 304-744-2253', '');
  154. // Remove MCI codes:
  155. body = body.replace(/%[A-Z0-9]{2}/g, '');
  156. // ^A (SOH) seems to need to produce CRLF in some cases??
  157. // body = body.replace(/\x01/g, '\r\n');
  158. // Reset and write the art:
  159. art.term.reset();
  160. art.term.write(body);
  161. art.term.cursorHidden = true;
  162. screen.render();
  163. if (process.argv[2] === '--debug' || process.argv[2] === '--save') {
  164. takeScreenshot(name);
  165. }
  166. });
  167. });
  168. });
  169. list.items.forEach(function(item, i) {
  170. var text = item.getText();
  171. item.setHover(map[text]);
  172. });
  173. list.focus();
  174. list.enterSelected(0);
  175. screen.key('h', function() {
  176. list.toggle();
  177. if (list.visible) list.focus();
  178. });
  179. screen.key('r', function() {
  180. shuffle();
  181. });
  182. screen.key('S-s', function() {
  183. takeScreenshot(list.ritems[list.selected]);
  184. });
  185. screen.key('s', function() {
  186. slideshow();
  187. });
  188. screen.key('q', function() {
  189. return process.exit(0);
  190. });
  191. screen.render();
  192. /**
  193. * Helpers
  194. */
  195. // https://github.com/chjj/blessed/issues/127
  196. // https://github.com/Mithgol/node-singlebyte
  197. function cp437ToUtf8(buf, callback) {
  198. try {
  199. return callback(null, singlebyte.bufToStr(buf, 'cp437'));
  200. } catch (e) {
  201. return callback(e);
  202. }
  203. }
  204. // Animating ANSI art doesn't work for screenshots.
  205. var ANIMATING = [
  206. 'bbs/void3',
  207. 'holiday/xmasfwks',
  208. 'unsorted/diver',
  209. 'unsorted/mash-chp',
  210. 'unsorted/ryans47',
  211. 'unsorted/xmasfwks'
  212. ];
  213. function takeScreenshot(name) {
  214. var filename = name.replace(/\//g, '.') + '.ans.sgr';
  215. var image;
  216. // Animating art hangs terminal during screenshot as of right now.
  217. if (~ANIMATING.indexOf(name)) {
  218. image = blessed.element.prototype.screenshot.call(art,
  219. 0 - art.ileft, art.width - art.iright,
  220. 0 - art.itop, art.height - art.ibottom);
  221. } else {
  222. image = art.screenshot();
  223. }
  224. fs.writeFileSync(__dirname + '/' + filename, image);
  225. msg.display('Screenshot taken.');
  226. }
  227. function slideshow() {
  228. if (!screen._.slideshow) {
  229. screen._.slideshow = setInterval(function slide() {
  230. if (screen.lockKeys) return;
  231. var i = (list.items.length - 1) * Math.random() | 0;
  232. list.enterSelected(i);
  233. return slide;
  234. }(), 3000);
  235. msg.display('Slideshow started.');
  236. } else {
  237. clearInterval(screen._.slideshow);
  238. delete screen._.slideshow;
  239. msg.display('Slideshow stopped.');
  240. }
  241. }
  242. function shuffle() {
  243. var items = Object.keys(map).sort(function(key) {
  244. return Math.random() > 0.5 ? 1 : -1;
  245. });
  246. list.setItems(items);
  247. screen.render();
  248. msg.display('Shuffled items.');
  249. }