terminal.js 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. /**
  2. * terminal.js - term.js terminal element for blessed
  3. * Copyright (c) 2013-2015, Christopher Jeffrey and contributors (MIT License).
  4. * https://github.com/chjj/blessed
  5. */
  6. /**
  7. * Modules
  8. */
  9. var nextTick = global.setImmediate || process.nextTick.bind(process);
  10. var Node = require('./node');
  11. var Box = require('./box');
  12. /**
  13. * Terminal
  14. */
  15. function Terminal(options) {
  16. if (!(this instanceof Node)) {
  17. return new Terminal(options);
  18. }
  19. options = options || {};
  20. options.scrollable = false;
  21. Box.call(this, options);
  22. // XXX Workaround for all motion
  23. if (this.screen.program.tmux && this.screen.program.tmuxVersion >= 2) {
  24. this.screen.program.enableMouse();
  25. }
  26. this.handler = options.handler;
  27. this.shell = options.shell || process.env.SHELL || 'sh';
  28. this.args = options.args || [];
  29. this.cursor = this.options.cursor;
  30. this.cursorBlink = this.options.cursorBlink;
  31. this.screenKeys = this.options.screenKeys;
  32. this.style = this.style || {};
  33. this.style.bg = this.style.bg || 'default';
  34. this.style.fg = this.style.fg || 'default';
  35. this.termName = options.terminal
  36. || options.term
  37. || process.env.TERM
  38. || 'xterm';
  39. this.bootstrap();
  40. }
  41. Terminal.prototype.__proto__ = Box.prototype;
  42. Terminal.prototype.type = 'terminal';
  43. Terminal.prototype.bootstrap = function() {
  44. var self = this;
  45. var element = {
  46. // window
  47. get document() { return element; },
  48. navigator: { userAgent: 'node.js' },
  49. // document
  50. get defaultView() { return element; },
  51. get documentElement() { return element; },
  52. createElement: function() { return element; },
  53. // element
  54. get ownerDocument() { return element; },
  55. addEventListener: function() {},
  56. removeEventListener: function() {},
  57. getElementsByTagName: function() { return [element]; },
  58. getElementById: function() { return element; },
  59. parentNode: null,
  60. offsetParent: null,
  61. appendChild: function() {},
  62. removeChild: function() {},
  63. setAttribute: function() {},
  64. getAttribute: function() {},
  65. style: {},
  66. focus: function() {},
  67. blur: function() {},
  68. console: console
  69. };
  70. element.parentNode = element;
  71. element.offsetParent = element;
  72. this.term = require('term.js')({
  73. termName: this.termName,
  74. cols: this.width - this.iwidth,
  75. rows: this.height - this.iheight,
  76. context: element,
  77. document: element,
  78. body: element,
  79. parent: element,
  80. cursorBlink: this.cursorBlink,
  81. screenKeys: this.screenKeys
  82. });
  83. this.term.refresh = function() {
  84. self.screen.render();
  85. };
  86. this.term.keyDown = function() {};
  87. this.term.keyPress = function() {};
  88. this.term.open(element);
  89. // Emits key sequences in html-land.
  90. // Technically not necessary here.
  91. // In reality if we wanted to be neat, we would overwrite the keyDown and
  92. // keyPress methods with our own node.js-keys->terminal-keys methods, but
  93. // since all the keys are already coming in as escape sequences, we can just
  94. // send the input directly to the handler/socket (see below).
  95. // this.term.on('data', function(data) {
  96. // self.handler(data);
  97. // });
  98. // Incoming keys and mouse inputs.
  99. // NOTE: Cannot pass mouse events - coordinates will be off!
  100. this.screen.program.input.on('data', this._onData = function(data) {
  101. if (self.screen.focused === self && !self._isMouse(data)) {
  102. self.handler(data);
  103. }
  104. });
  105. this.onScreenEvent('mouse', function(data) {
  106. if (self.screen.focused !== self) return;
  107. if (data.x < self.aleft + self.ileft) return;
  108. if (data.y < self.atop + self.itop) return;
  109. if (data.x > self.aleft - self.ileft + self.width) return;
  110. if (data.y > self.atop - self.itop + self.height) return;
  111. if (self.term.x10Mouse
  112. || self.term.vt200Mouse
  113. || self.term.normalMouse
  114. || self.term.mouseEvents
  115. || self.term.utfMouse
  116. || self.term.sgrMouse
  117. || self.term.urxvtMouse) {
  118. ;
  119. } else {
  120. return;
  121. }
  122. var b = data.raw[0]
  123. , x = data.x - self.aleft
  124. , y = data.y - self.atop
  125. , s;
  126. if (self.term.urxvtMouse) {
  127. if (self.screen.program.sgrMouse) {
  128. b += 32;
  129. }
  130. s = '\x1b[' + b + ';' + (x + 32) + ';' + (y + 32) + 'M';
  131. } else if (self.term.sgrMouse) {
  132. if (!self.screen.program.sgrMouse) {
  133. b -= 32;
  134. }
  135. s = '\x1b[<' + b + ';' + x + ';' + y
  136. + (data.action === 'mousedown' ? 'M' : 'm');
  137. } else {
  138. if (self.screen.program.sgrMouse) {
  139. b += 32;
  140. }
  141. s = '\x1b[M'
  142. + String.fromCharCode(b)
  143. + String.fromCharCode(x + 32)
  144. + String.fromCharCode(y + 32);
  145. }
  146. self.handler(s);
  147. });
  148. this.on('focus', function() {
  149. self.term.focus();
  150. });
  151. this.on('blur', function() {
  152. self.term.blur();
  153. });
  154. this.term.on('title', function(title) {
  155. self.title = title;
  156. self.emit('title', title);
  157. });
  158. this.term.on('passthrough', function(data) {
  159. self.screen.program.flush();
  160. self.screen.program._owrite(data);
  161. });
  162. this.on('resize', function() {
  163. nextTick(function() {
  164. self.term.resize(self.width - self.iwidth, self.height - self.iheight);
  165. });
  166. });
  167. this.once('render', function() {
  168. self.term.resize(self.width - self.iwidth, self.height - self.iheight);
  169. });
  170. this.on('destroy', function() {
  171. self.kill();
  172. self.screen.program.input.removeListener('data', self._onData);
  173. });
  174. if (this.handler) {
  175. return;
  176. }
  177. this.pty = require('pty.js').fork(this.shell, this.args, {
  178. name: this.termName,
  179. cols: this.width - this.iwidth,
  180. rows: this.height - this.iheight,
  181. cwd: process.env.HOME,
  182. env: this.options.env || process.env
  183. });
  184. this.on('resize', function() {
  185. nextTick(function() {
  186. try {
  187. self.pty.resize(self.width - self.iwidth, self.height - self.iheight);
  188. } catch (e) {
  189. ;
  190. }
  191. });
  192. });
  193. this.handler = function(data) {
  194. self.pty.write(data);
  195. self.screen.render();
  196. };
  197. this.pty.on('data', function(data) {
  198. self.write(data);
  199. self.screen.render();
  200. });
  201. this.pty.on('exit', function(code) {
  202. self.emit('exit', code || null);
  203. });
  204. this.onScreenEvent('keypress', function() {
  205. self.screen.render();
  206. });
  207. this.screen._listenKeys(this);
  208. };
  209. Terminal.prototype.write = function(data) {
  210. return this.term.write(data);
  211. };
  212. Terminal.prototype.render = function() {
  213. var ret = this._render();
  214. if (!ret) return;
  215. this.dattr = this.sattr(this.style);
  216. var xi = ret.xi + this.ileft
  217. , xl = ret.xl - this.iright
  218. , yi = ret.yi + this.itop
  219. , yl = ret.yl - this.ibottom
  220. , cursor;
  221. var scrollback = this.term.lines.length - (yl - yi);
  222. for (var y = Math.max(yi, 0); y < yl; y++) {
  223. var line = this.screen.lines[y];
  224. if (!line || !this.term.lines[scrollback + y - yi]) break;
  225. if (y === yi + this.term.y
  226. && this.term.cursorState
  227. && this.screen.focused === this
  228. && (this.term.ydisp === this.term.ybase || this.term.selectMode)
  229. && !this.term.cursorHidden) {
  230. cursor = xi + this.term.x;
  231. } else {
  232. cursor = -1;
  233. }
  234. for (var x = Math.max(xi, 0); x < xl; x++) {
  235. if (!line[x] || !this.term.lines[scrollback + y - yi][x - xi]) break;
  236. line[x][0] = this.term.lines[scrollback + y - yi][x - xi][0];
  237. if (x === cursor) {
  238. if (this.cursor === 'line') {
  239. line[x][0] = this.dattr;
  240. line[x][1] = '\u2502';
  241. continue;
  242. } else if (this.cursor === 'underline') {
  243. line[x][0] = this.dattr | (2 << 18);
  244. } else if (this.cursor === 'block' || !this.cursor) {
  245. line[x][0] = this.dattr | (8 << 18);
  246. }
  247. }
  248. line[x][1] = this.term.lines[scrollback + y - yi][x - xi][1];
  249. // default foreground = 257
  250. if (((line[x][0] >> 9) & 0x1ff) === 257) {
  251. line[x][0] &= ~(0x1ff << 9);
  252. line[x][0] |= ((this.dattr >> 9) & 0x1ff) << 9;
  253. }
  254. // default background = 256
  255. if ((line[x][0] & 0x1ff) === 256) {
  256. line[x][0] &= ~0x1ff;
  257. line[x][0] |= this.dattr & 0x1ff;
  258. }
  259. }
  260. line.dirty = true;
  261. }
  262. return ret;
  263. };
  264. Terminal.prototype._isMouse = function(buf) {
  265. var s = buf;
  266. if (Buffer.isBuffer(s)) {
  267. if (s[0] > 127 && s[1] === undefined) {
  268. s[0] -= 128;
  269. s = '\x1b' + s.toString('utf-8');
  270. } else {
  271. s = s.toString('utf-8');
  272. }
  273. }
  274. return (buf[0] === 0x1b && buf[1] === 0x5b && buf[2] === 0x4d)
  275. || /^\x1b\[M([\x00\u0020-\uffff]{3})/.test(s)
  276. || /^\x1b\[(\d+;\d+;\d+)M/.test(s)
  277. || /^\x1b\[<(\d+;\d+;\d+)([mM])/.test(s)
  278. || /^\x1b\[<(\d+;\d+;\d+;\d+)&w/.test(s)
  279. || /^\x1b\[24([0135])~\[(\d+),(\d+)\]\r/.test(s)
  280. || /^\x1b\[(O|I)/.test(s);
  281. };
  282. Terminal.prototype.setScroll =
  283. Terminal.prototype.scrollTo = function(offset) {
  284. this.term.ydisp = offset;
  285. return this.emit('scroll');
  286. };
  287. Terminal.prototype.getScroll = function() {
  288. return this.term.ydisp;
  289. };
  290. Terminal.prototype.scroll = function(offset) {
  291. this.term.scrollDisp(offset);
  292. return this.emit('scroll');
  293. };
  294. Terminal.prototype.resetScroll = function() {
  295. this.term.ydisp = 0;
  296. this.term.ybase = 0;
  297. return this.emit('scroll');
  298. };
  299. Terminal.prototype.getScrollHeight = function() {
  300. return this.term.rows - 1;
  301. };
  302. Terminal.prototype.getScrollPerc = function() {
  303. return (this.term.ydisp / this.term.ybase) * 100;
  304. };
  305. Terminal.prototype.setScrollPerc = function(i) {
  306. return this.setScroll((i / 100) * this.term.ybase | 0);
  307. };
  308. Terminal.prototype.screenshot = function(xi, xl, yi, yl) {
  309. xi = 0 + (xi || 0);
  310. if (xl != null) {
  311. xl = 0 + (xl || 0);
  312. } else {
  313. xl = this.term.lines[0].length;
  314. }
  315. yi = 0 + (yi || 0);
  316. if (yl != null) {
  317. yl = 0 + (yl || 0);
  318. } else {
  319. yl = this.term.lines.length;
  320. }
  321. return this.screen.screenshot(xi, xl, yi, yl, this.term);
  322. };
  323. Terminal.prototype.kill = function() {
  324. if (this.pty) {
  325. this.pty.destroy();
  326. this.pty.kill();
  327. }
  328. this.term.refresh = function() {};
  329. this.term.write('\x1b[H\x1b[J');
  330. if (this.term._blink) {
  331. clearInterval(this.term._blink);
  332. }
  333. this.term.destroy();
  334. };
  335. /**
  336. * Expose
  337. */
  338. module.exports = Terminal;