PageRenderTime 63ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 1ms

/lib/widget.js

https://github.com/prodigeni/blessed
JavaScript | 6515 lines | 4836 code | 1083 blank | 596 comment | 1454 complexity | f83a8ac093afdd483465a17b444e6819 MD5 | raw file

Large files files are truncated, but you can click here to view the full file

  1. /**
  2. * widget.js - high-level interface for blessed
  3. * Copyright (c) 2013, Christopher Jeffrey (MIT License)
  4. * https://github.com/chjj/blessed
  5. * Still under heavy development.
  6. */
  7. /**
  8. * Modules
  9. */
  10. var EventEmitter = require('./events').EventEmitter
  11. , assert = require('assert')
  12. , path = require('path')
  13. , fs = require('fs');
  14. var colors = require('./colors')
  15. , program = require('./program')
  16. , widget = exports;
  17. /**
  18. * Node
  19. */
  20. function Node(options) {
  21. if (!(this instanceof Node)) {
  22. return new Node(options);
  23. }
  24. EventEmitter.call(this);
  25. options = options || {};
  26. this.options = options;
  27. this.screen = this.screen
  28. || options.screen
  29. || Screen.global
  30. || (function(){throw new Error('No active screen.')})();
  31. this.parent = options.parent || null;
  32. this.children = [];
  33. this.$ = this._ = this.data = {};
  34. this.uid = Node.uid++;
  35. this.index = -1;
  36. if (this.type !== 'screen') {
  37. this.detached = true;
  38. }
  39. if (this.parent) {
  40. this.parent.append(this);
  41. }
  42. (options.children || []).forEach(this.append.bind(this));
  43. }
  44. Node.uid = 0;
  45. Node.prototype.__proto__ = EventEmitter.prototype;
  46. Node.prototype.type = 'node';
  47. Node.prototype.insert = function(element, i) {
  48. var self = this;
  49. element.detach();
  50. element.parent = this;
  51. if (i === 0) {
  52. this.children.unshift(element);
  53. } else if (i === this.children.length) {
  54. this.children.push(element);
  55. } else {
  56. this.children.splice(i, 0, element);
  57. }
  58. element.emit('reparent', this);
  59. this.emit('adopt', element);
  60. (function emit(el) {
  61. var n = el.detached !== self.detached;
  62. el.detached = self.detached;
  63. if (n) el.emit('attach');
  64. el.children.forEach(emit);
  65. })(element);
  66. if (!this.screen.focused) {
  67. this.screen.focused = element;
  68. }
  69. };
  70. Node.prototype.prepend = function(element) {
  71. this.insert(element, 0);
  72. };
  73. Node.prototype.append = function(element) {
  74. this.insert(element, this.children.length);
  75. };
  76. Node.prototype.insertBefore = function(element, other) {
  77. var i = this.children.indexOf(other);
  78. if (~i) this.insert(element, i);
  79. };
  80. Node.prototype.insertAfter = function(element, other) {
  81. var i = this.children.indexOf(other);
  82. if (~i) this.insert(element, i + 1);
  83. };
  84. Node.prototype.remove = function(element) {
  85. if (element.parent !== this) return;
  86. var i = this.children.indexOf(element);
  87. if (!~i) return;
  88. element.clearPos();
  89. element.parent = null;
  90. this.children.splice(i, 1);
  91. i = this.screen.clickable.indexOf(element);
  92. if (~i) this.screen.clickable.splice(i, 1);
  93. i = this.screen.keyable.indexOf(element);
  94. if (~i) this.screen.keyable.splice(i, 1);
  95. element.emit('reparent', null);
  96. this.emit('remove', element);
  97. (function emit(el) {
  98. var n = el.detached !== true;
  99. el.detached = true;
  100. if (n) el.emit('detach');
  101. el.children.forEach(emit);
  102. })(element);
  103. if (this.screen.focused === element) {
  104. this.screen.rewindFocus();
  105. }
  106. };
  107. Node.prototype.detach = function() {
  108. if (this.parent) this.parent.remove(this);
  109. };
  110. Node.prototype.forDescendants = function(iter, s) {
  111. if (s) iter(this);
  112. this.children.forEach(function emit(el) {
  113. iter(el);
  114. el.children.forEach(emit);
  115. });
  116. };
  117. Node.prototype.forAncestors = function(iter, s) {
  118. var el = this;
  119. if (s) iter(this);
  120. while (el = el.parent) {
  121. iter(el);
  122. }
  123. };
  124. Node.prototype.collectDescendants = function(s) {
  125. var out = [];
  126. this.forDescendants(function(el) {
  127. out.push(el);
  128. }, s);
  129. return out;
  130. };
  131. Node.prototype.collectAncestors = function(s) {
  132. var out = [];
  133. this.forAncestors(function(el) {
  134. out.push(el);
  135. }, s);
  136. return out;
  137. };
  138. Node.prototype.emitDescendants = function() {
  139. var args = Array.prototype.slice(arguments)
  140. , iter;
  141. if (typeof args[args.length-1] === 'function') {
  142. iter = args.pop();
  143. }
  144. return this.forDescendants(function(el) {
  145. if (iter) iter(el);
  146. el.emit.apply(el, args);
  147. }, true);
  148. };
  149. Node.prototype.emitAncestors = function() {
  150. var args = Array.prototype.slice(arguments)
  151. , iter;
  152. if (typeof args[args.length-1] === 'function') {
  153. iter = args.pop();
  154. }
  155. return this.forAncestors(function(el) {
  156. if (iter) iter(el);
  157. el.emit.apply(el, args);
  158. }, true);
  159. };
  160. Node.prototype.hasDescendant = function(target) {
  161. return (function find(el) {
  162. for (var i = 0; i < el.children.length; i++) {
  163. if (el.children[i] === target) {
  164. return true;
  165. }
  166. if (find(el.children[i]) === true) {
  167. return true;
  168. }
  169. }
  170. return false;
  171. })(this);
  172. };
  173. Node.prototype.hasAncestor = function(target) {
  174. var el = this;
  175. while (el = el.parent) {
  176. if (el === target) return true;
  177. }
  178. return false;
  179. };
  180. Node.prototype.get = function(name, value) {
  181. if (this.data.hasOwnProperty(name)) {
  182. return this.data[name];
  183. }
  184. return value;
  185. };
  186. Node.prototype.set = function(name, value) {
  187. return this.data[name] = value;
  188. };
  189. /**
  190. * Screen
  191. */
  192. function Screen(options) {
  193. var self = this;
  194. if (!(this instanceof Node)) {
  195. return new Screen(options);
  196. }
  197. options = options || {};
  198. if (options.rsety && options.listen) {
  199. options = { program: options };
  200. }
  201. this.program = options.program || program.global;
  202. if (!this.program) {
  203. this.program = program({
  204. input: options.input,
  205. output: options.output,
  206. log: options.log,
  207. debug: options.debug,
  208. dump: options.dump,
  209. term: options.term,
  210. tput: true,
  211. buffer: true,
  212. zero: true
  213. });
  214. } else {
  215. this.program.setupTput();
  216. this.program.useBuffer = true;
  217. this.program.zero = true;
  218. }
  219. this.tput = this.program.tput;
  220. if (!Screen.global) {
  221. Screen.global = this;
  222. }
  223. Node.call(this, options);
  224. this.autoPadding = options.autoPadding;
  225. this.tabc = Array((options.tabSize || 4) + 1).join(' ');
  226. this.ignoreLocked = options.ignoreLocked || [];
  227. this.dattr = ((0 << 18) | (0x1ff << 9)) | 0x1ff;
  228. this.renders = 0;
  229. this.position = {
  230. left: this.left = this.rleft = 0,
  231. right: this.right = this.rright = 0,
  232. top: this.top = this.rtop = 0,
  233. bottom: this.bottom = this.rbottom = 0
  234. //get height() { return self.height; },
  235. //get width() { return self.width; }
  236. };
  237. this.ileft = 0;
  238. this.itop = 0;
  239. this.iright = 0;
  240. this.ibottom = 0;
  241. this.iheight = 0;
  242. this.iwidth = 0;
  243. this.padding = {
  244. left: 0,
  245. top: 0,
  246. right: 0,
  247. bottom: 0
  248. };
  249. this.hover = null;
  250. this.history = [];
  251. this.clickable = [];
  252. this.keyable = [];
  253. this.grabKeys = false;
  254. this.lockKeys = false;
  255. this.focused;
  256. this._buf = '';
  257. this._ci = -1;
  258. function resize() {
  259. self.alloc();
  260. self.render();
  261. (function emit(el) {
  262. el.emit('resize');
  263. el.children.forEach(emit);
  264. })(self);
  265. }
  266. this.program.on('resize', function() {
  267. if (!self.options.resizeTimeout) {
  268. return resize();
  269. }
  270. if (self._resizeTimer) {
  271. clearTimeout(self._resizeTimer);
  272. delete self._resizeTimer;
  273. }
  274. var time = typeof self.options.resizeTimeout === 'number'
  275. ? self.options.resizeTimeout
  276. : 300;
  277. self._resizeTimer = setTimeout(resize, time);
  278. });
  279. this.program.on('focus', function() {
  280. self.emit('focus');
  281. });
  282. this.program.on('blur', function() {
  283. self.emit('blur');
  284. });
  285. this.enter();
  286. function reset() {
  287. if (reset.done) return;
  288. reset.done = true;
  289. self.leave();
  290. }
  291. this._maxListeners = Infinity;
  292. if (this.options.handleUncaughtExceptions !== false) {
  293. process.on('uncaughtException', function(err) {
  294. reset();
  295. if (err) console.error(err.stack ? err.stack + '' : err + '');
  296. return process.exit(1);
  297. });
  298. }
  299. process.on('SIGTERM', function() {
  300. return process.exit(0);
  301. });
  302. process.on('exit', function() {
  303. reset();
  304. });
  305. this.on('newListener', function fn(type) {
  306. if (type === 'keypress' || type.indexOf('key ') === 0 || type === 'mouse') {
  307. if (type === 'keypress' || type.indexOf('key ') === 0) self._listenKeys();
  308. if (type === 'mouse') self._listenMouse();
  309. }
  310. if (type === 'mouse'
  311. || type === 'click'
  312. || type === 'mouseover'
  313. || type === 'mouseout'
  314. || type === 'mousedown'
  315. || type === 'mouseup'
  316. || type === 'mousewheel'
  317. || type === 'wheeldown'
  318. || type === 'wheelup'
  319. || type === 'mousemove') {
  320. self._listenMouse();
  321. }
  322. });
  323. }
  324. Screen.global = null;
  325. Screen.prototype.__proto__ = Node.prototype;
  326. Screen.prototype.type = 'screen';
  327. Screen.prototype.enter = function() {
  328. if (this.program.isAlt) return;
  329. this.program.alternateBuffer();
  330. this.program.hideCursor();
  331. this.program.cup(0, 0);
  332. //this.program.csr(0, this.height - 1);
  333. this.alloc();
  334. };
  335. Screen.prototype.leave = function() {
  336. if (!this.program.isAlt) return;
  337. if (this.program.scrollTop !== 0
  338. || this.program.scrollBottom !== this.rows - 1) {
  339. this.program.csr(0, this.height - 1);
  340. }
  341. // this.program.clear();
  342. this.alloc();
  343. this.program.showCursor();
  344. this.program.normalBuffer();
  345. if (this._listenedMouse) {
  346. this.program.disableMouse();
  347. }
  348. this.program.flush();
  349. };
  350. Screen.prototype.log = function() {
  351. return this.program.log.apply(this.program, arguments);
  352. };
  353. Screen.prototype.debug = function() {
  354. return this.program.debug.apply(this.program, arguments);
  355. };
  356. Screen.prototype._listenMouse = function(el) {
  357. var self = this;
  358. if (el && !~this.clickable.indexOf(el)) {
  359. el.clickable = true;
  360. this.clickable.push(el);
  361. }
  362. if (this._listenedMouse) return;
  363. this._listenedMouse = true;
  364. this.program.enableMouse();
  365. this.on('render', function() {
  366. self._needsClickableSort = true;
  367. });
  368. this.program.on('mouse', function(data) {
  369. if (self.lockKeys) return;
  370. if (self._needsClickableSort) {
  371. self.clickable = hsort(self.clickable);
  372. self._needsClickableSort = false;
  373. }
  374. var i = 0
  375. , el
  376. , set
  377. , pos;
  378. for (; i < self.clickable.length; i++) {
  379. el = self.clickable[i];
  380. if (el.detached || !el.visible) {
  381. continue;
  382. }
  383. // if (self.grabMouse && self.focused !== el
  384. // && !el.hasAncestor(self.focused)) continue;
  385. pos = el.lpos;
  386. if (!pos) continue;
  387. if (data.x >= pos.xi && data.x < pos.xl
  388. && data.y >= pos.yi && data.y < pos.yl) {
  389. el.emit('mouse', data);
  390. if (data.action === 'mousedown') {
  391. self.mouseDown = el;
  392. } else if (data.action === 'mouseup') {
  393. (self.mouseDown || el).emit('click', data);
  394. self.mouseDown = null;
  395. } else if (data.action === 'mousemove') {
  396. if (self.hover && el.index > self.hover.index) {
  397. set = false;
  398. }
  399. if (self.hover !== el && !set) {
  400. if (self.hover) {
  401. self.hover.emit('mouseout', data);
  402. }
  403. el.emit('mouseover', data);
  404. self.hover = el;
  405. }
  406. set = true;
  407. }
  408. el.emit(data.action, data);
  409. break;
  410. }
  411. }
  412. // Just mouseover?
  413. if ((data.action === 'mousemove'
  414. || data.action === 'mousedown'
  415. || data.action === 'mouseup')
  416. && self.hover
  417. && !set) {
  418. self.hover.emit('mouseout', data);
  419. self.hover = null;
  420. }
  421. self.emit('mouse', data);
  422. });
  423. // Autofocus highest element.
  424. // this.on('element click', function(el, data) {
  425. // var target;
  426. // do {
  427. // if (el.clickable === true && el.options.autoFocus !== false) {
  428. // target = el;
  429. // }
  430. // } while (el = el.parent);
  431. // if (target) target.focus();
  432. // });
  433. // Autofocus elements with the appropriate option.
  434. this.on('element click', function(el, data) {
  435. if (el.clickable === true && el.options.autoFocus !== false) {
  436. el.focus();
  437. }
  438. });
  439. };
  440. Screen.prototype._listenKeys = function(el) {
  441. var self = this;
  442. if (el && !~this.keyable.indexOf(el)) {
  443. el.keyable = true;
  444. this.keyable.push(el);
  445. }
  446. if (this._listenedKeys) return;
  447. this._listenedKeys = true;
  448. // NOTE: The event emissions used to be reversed:
  449. // element + screen
  450. // They are now:
  451. // screen + element
  452. // After the first keypress emitted, the handler
  453. // checks to make sure grabKeys, lockKeys, and focused
  454. // weren't changed, and handles those situations appropriately.
  455. this.program.on('keypress', function(ch, key) {
  456. if (self.lockKeys && !~self.ignoreLocked.indexOf(key.full)) {
  457. return;
  458. }
  459. var focused = self.focused
  460. , grabKeys = self.grabKeys;
  461. if (!grabKeys) {
  462. self.emit('keypress', ch, key);
  463. self.emit('key ' + key.full, ch, key);
  464. }
  465. // If something changed from the screen key handler, stop.
  466. if (self.grabKeys !== grabKeys || self.lockKeys) {
  467. return;
  468. }
  469. if (focused && focused.keyable) {
  470. focused.emit('keypress', ch, key);
  471. focused.emit('key ' + key.full, ch, key);
  472. }
  473. });
  474. };
  475. Screen.prototype.__defineGetter__('cols', function() {
  476. return this.program.cols;
  477. });
  478. Screen.prototype.__defineGetter__('rows', function() {
  479. return this.program.rows;
  480. });
  481. Screen.prototype.__defineGetter__('width', function() {
  482. return this.program.cols;
  483. });
  484. Screen.prototype.__defineGetter__('height', function() {
  485. return this.program.rows;
  486. });
  487. Screen.prototype.alloc = function() {
  488. var x, y;
  489. this.lines = [];
  490. for (y = 0; y < this.rows; y++) {
  491. this.lines[y] = [];
  492. for (x = 0; x < this.cols; x++) {
  493. this.lines[y][x] = [this.dattr, ' '];
  494. }
  495. this.lines[y].dirty = false;
  496. }
  497. this.olines = [];
  498. for (y = 0; y < this.rows; y++) {
  499. this.olines[y] = [];
  500. for (x = 0; x < this.cols; x++) {
  501. this.olines[y][x] = [this.dattr, ' '];
  502. }
  503. }
  504. this.program.clear();
  505. };
  506. Screen.prototype.render = function() {
  507. var self = this;
  508. this.emit('prerender');
  509. // TODO: Possibly get rid of .dirty altogether.
  510. // TODO: Could possibly drop .dirty and just clear the `lines` buffer every
  511. // time before a screen.render. This way clearRegion doesn't have to be
  512. // called in arbitrary places for the sake of clearing a spot where an
  513. // element used to be (e.g. when an element moves or is hidden). There could
  514. // be some overhead though.
  515. // this.screen.clearRegion(0, this.cols, 0, this.rows);
  516. this._ci = 0;
  517. this.children.forEach(function(el) {
  518. el.index = self._ci++;
  519. //el._rendering = true;
  520. el.render();
  521. //el._rendering = false;
  522. });
  523. this._ci = -1;
  524. this.draw(0, this.rows - 1);
  525. // XXX Workaround to deal with cursor pos before the screen has rendered and
  526. // lpos is not reliable (stale).
  527. if (this.focused && this.focused._updateCursor) {
  528. this.focused._updateCursor(true);
  529. }
  530. this.renders++;
  531. this.emit('render');
  532. };
  533. Screen.prototype.blankLine = function(ch, dirty) {
  534. var out = [];
  535. for (var x = 0; x < this.cols; x++) {
  536. out[x] = [this.dattr, ch || ' '];
  537. }
  538. out.dirty = dirty;
  539. return out;
  540. };
  541. Screen.prototype.insertLine = function(n, y, top, bottom) {
  542. // if (y === top) return this.insertLineNC(n, y, top, bottom);
  543. if (!this.tput.strings.change_scroll_region
  544. || !this.tput.strings.delete_line
  545. || !this.tput.strings.insert_line) return;
  546. this._buf += this.tput.csr(top, bottom);
  547. this._buf += this.tput.cup(y, 0);
  548. this._buf += this.tput.il(n);
  549. this._buf += this.tput.csr(0, this.height - 1);
  550. var j = bottom + 1;
  551. while (n--) {
  552. this.lines.splice(y, 0, this.blankLine());
  553. this.lines.splice(j, 1);
  554. this.olines.splice(y, 0, this.blankLine());
  555. this.olines.splice(j, 1);
  556. }
  557. };
  558. Screen.prototype.deleteLine = function(n, y, top, bottom) {
  559. // if (y === top) return this.deleteLineNC(n, y, top, bottom);
  560. if (!this.tput.strings.change_scroll_region
  561. || !this.tput.strings.delete_line
  562. || !this.tput.strings.insert_line) return;
  563. this._buf += this.tput.csr(top, bottom);
  564. this._buf += this.tput.cup(y, 0);
  565. this._buf += this.tput.dl(n);
  566. this._buf += this.tput.csr(0, this.height - 1);
  567. var j = bottom + 1;
  568. while (n--) {
  569. this.lines.splice(j, 0, this.blankLine());
  570. this.lines.splice(y, 1);
  571. this.olines.splice(j, 0, this.blankLine());
  572. this.olines.splice(y, 1);
  573. }
  574. };
  575. // This is how ncurses does it.
  576. // Scroll down (up cursor-wise).
  577. // This will only work for top line deletion as opposed to arbitrary lines.
  578. Screen.prototype.insertLineNC = function(n, y, top, bottom) {
  579. if (!this.tput.strings.change_scroll_region
  580. || !this.tput.strings.delete_line) return;
  581. this._buf += this.tput.csr(top, bottom);
  582. this._buf += this.tput.cup(top, 0);
  583. this._buf += this.tput.dl(n);
  584. this._buf += this.tput.csr(0, this.height - 1);
  585. var j = bottom + 1;
  586. while (n--) {
  587. this.lines.splice(j, 0, this.blankLine());
  588. this.lines.splice(y, 1);
  589. this.olines.splice(j, 0, this.blankLine());
  590. this.olines.splice(y, 1);
  591. }
  592. };
  593. // This is how ncurses does it.
  594. // Scroll up (down cursor-wise).
  595. // This will only work for bottom line deletion as opposed to arbitrary lines.
  596. Screen.prototype.deleteLineNC = function(n, y, top, bottom) {
  597. if (!this.tput.strings.change_scroll_region
  598. || !this.tput.strings.delete_line) return;
  599. this._buf += this.tput.csr(top, bottom);
  600. this._buf += this.tput.cup(bottom, 0);
  601. this._buf += Array(n + 1).join('\n');
  602. this._buf += this.tput.csr(0, this.height - 1);
  603. var j = bottom + 1;
  604. while (n--) {
  605. this.lines.splice(j, 0, this.blankLine());
  606. this.lines.splice(y, 1);
  607. this.olines.splice(j, 0, this.blankLine());
  608. this.olines.splice(y, 1);
  609. }
  610. };
  611. Screen.prototype.insertBottom = function(top, bottom) {
  612. return this.deleteLine(1, top, top, bottom);
  613. };
  614. Screen.prototype.insertTop = function(top, bottom) {
  615. return this.insertLine(1, top, top, bottom);
  616. };
  617. Screen.prototype.deleteBottom = function(top, bottom) {
  618. return this.clearRegion(0, this.width, bottom, bottom);
  619. };
  620. Screen.prototype.deleteTop = function(top, bottom) {
  621. // Same as: return this.insertBottom(top, bottom);
  622. return this.deleteLine(1, top, top, bottom);
  623. };
  624. // Parse the sides of an element to determine
  625. // whether an element has uniform cells on
  626. // both sides. If it does, we can use CSR to
  627. // optimize scrolling on a scrollable element.
  628. // Not exactly sure how worthwile this is.
  629. // This will cause a performance/cpu-usage hit,
  630. // but will it be less or greater than the
  631. // performance hit of slow-rendering scrollable
  632. // boxes with clean sides?
  633. Screen.prototype.cleanSides = function(el) {
  634. var pos = el.lpos;
  635. if (!pos) {
  636. return false;
  637. }
  638. if (pos._cleanSides != null) {
  639. return pos._cleanSides;
  640. }
  641. if (pos.xi === 0 && pos.xl === this.width) {
  642. return pos._cleanSides = true;
  643. }
  644. if (this.options.fastCSR) {
  645. // Maybe just do this instead of parsing.
  646. if (this.width - (pos.xl - pos.xi) < 40) {
  647. return pos._cleanSides = true;
  648. }
  649. return pos._cleanSides = false;
  650. }
  651. if (!this.options.smartCSR) {
  652. return false;
  653. }
  654. // The scrollbar can't update properly, and there's also a
  655. // chance that the scrollbar may get moved around senselessly.
  656. // NOTE: In pratice, this doesn't seem to be the case.
  657. // if (this.scrollbar) {
  658. // return pos._cleanSides = false;
  659. // }
  660. // Doesn't matter if we're only a height of 1.
  661. // if ((pos.yl - el.ibottom) - (pos.yi + el.itop) <= 1) {
  662. // return pos._cleanSides = false;
  663. // }
  664. var yi = pos.yi + el.itop
  665. , yl = pos.yl - el.ibottom
  666. , first
  667. , ch
  668. , x
  669. , y;
  670. for (x = pos.xi - 1; x >= 0; x--) {
  671. first = this.olines[yi][x];
  672. for (y = yi; y < yl; y++) {
  673. ch = this.olines[y][x];
  674. if (ch[0] !== first[0] || ch[1] !== first[1]) {
  675. return pos._cleanSides = false;
  676. }
  677. }
  678. }
  679. for (x = pos.xl; x < this.width; x++) {
  680. first = this.olines[yi][x];
  681. for (y = yi; y < yl; y++) {
  682. ch = this.olines[y][x];
  683. if (ch[0] !== first[0] || ch[1] !== first[1]) {
  684. return pos._cleanSides = false;
  685. }
  686. }
  687. }
  688. return pos._cleanSides = true;
  689. };
  690. Screen.prototype.draw = function(start, end) {
  691. // this.emit('predraw');
  692. var x
  693. , y
  694. , line
  695. , out
  696. , ch
  697. , data
  698. , attr
  699. , fg
  700. , bg
  701. , flags;
  702. var main = ''
  703. , pre
  704. , post;
  705. var clr
  706. , neq
  707. , xx;
  708. var lx = -1
  709. , ly = -1
  710. , o;
  711. var acs;
  712. if (this._buf) {
  713. main += this._buf;
  714. this._buf = '';
  715. }
  716. for (y = start; y <= end; y++) {
  717. line = this.lines[y];
  718. o = this.olines[y];
  719. if (!line.dirty) continue;
  720. line.dirty = false;
  721. out = '';
  722. attr = this.dattr;
  723. for (x = 0; x < this.cols; x++) {
  724. data = line[x][0];
  725. ch = line[x][1];
  726. // Take advantage of xterm's back_color_erase feature by using a
  727. // lookahead. Stop spitting out so many damn spaces. NOTE: Is checking
  728. // the bg for non BCE terminals worth the overhead?
  729. if (this.options.useBCE
  730. && ch === ' '
  731. && (this.tput.bools.back_color_erase
  732. || (data & 0x1ff) === (this.dattr & 0x1ff))
  733. && ((data >> 18) & 8) === ((this.dattr >> 18) & 8)) {
  734. clr = true;
  735. neq = false;
  736. for (xx = x; xx < this.cols; xx++) {
  737. if (line[xx][0] !== data || line[xx][1] !== ' ') {
  738. clr = false;
  739. break;
  740. }
  741. if (line[xx][0] !== o[xx][0] || line[xx][1] !== o[xx][1]) {
  742. neq = true;
  743. }
  744. }
  745. if (clr && neq) {
  746. lx = -1, ly = -1;
  747. if (data !== attr) {
  748. out += this.codeAttr(data);
  749. attr = data;
  750. }
  751. out += this.tput.cup(y, x);
  752. out += this.tput.el();
  753. for (xx = x; xx < this.cols; xx++) {
  754. o[xx][0] = data;
  755. o[xx][1] = ' ';
  756. }
  757. break;
  758. }
  759. // If there's more than 10 spaces, use EL regardless
  760. // and start over drawing the rest of line. Might
  761. // not be worth it. Try to use ECH if the terminal
  762. // supports it. Maybe only try to use ECH here.
  763. // //if (this.tput.strings.erase_chars)
  764. // if (!clr && neq && (xx - x) > 10) {
  765. // lx = -1, ly = -1;
  766. // if (data !== attr) {
  767. // out += this.codeAttr(data);
  768. // attr = data;
  769. // }
  770. // out += this.tput.cup(y, x);
  771. // if (this.tput.strings.erase_chars) {
  772. // // Use erase_chars to avoid erasing the whole line.
  773. // out += this.tput.ech(xx - x);
  774. // } else {
  775. // out += this.tput.el();
  776. // }
  777. // out += this.tput.cuf(xx - x);
  778. // this.fillRegion(data, ' ',
  779. // x, this.tput.strings.erase_chars ? xx : this.cols,
  780. // y, y + 1);
  781. // x = xx - 1;
  782. // continue;
  783. // }
  784. // Skip to the next line if the
  785. // rest of the line is already drawn.
  786. // if (!neq) {
  787. // for (; xx < this.cols; xx++) {
  788. // if (line[xx][0] !== o[xx][0] || line[xx][1] !== o[xx][1]) {
  789. // neq = true;
  790. // break;
  791. // }
  792. // }
  793. // if (!neq) {
  794. // attr = data;
  795. // break;
  796. // }
  797. // }
  798. }
  799. // Optimize by comparing the real output
  800. // buffer to the pending output buffer.
  801. if (data === o[x][0] && ch === o[x][1]) {
  802. if (lx === -1) {
  803. lx = x;
  804. ly = y;
  805. }
  806. continue;
  807. } else if (lx !== -1) {
  808. out += y === ly
  809. ? this.tput.cuf(x - lx)
  810. : this.tput.cup(y, x);
  811. lx = -1, ly = -1;
  812. }
  813. o[x][0] = data;
  814. o[x][1] = ch;
  815. if (data !== attr) {
  816. if (attr !== this.dattr) {
  817. out += '\x1b[m';
  818. }
  819. if (data !== this.dattr) {
  820. out += '\x1b[';
  821. bg = data & 0x1ff;
  822. fg = (data >> 9) & 0x1ff;
  823. flags = data >> 18;
  824. // bold
  825. if (flags & 1) {
  826. out += '1;';
  827. }
  828. // underline
  829. if (flags & 2) {
  830. out += '4;';
  831. }
  832. // blink
  833. if (flags & 4) {
  834. out += '5;';
  835. }
  836. // inverse
  837. if (flags & 8) {
  838. out += '7;';
  839. }
  840. // invisible
  841. if (flags & 16) {
  842. out += '8;';
  843. }
  844. if (bg !== 0x1ff) {
  845. bg = this._reduceColor(bg);
  846. if (bg < 16) {
  847. if (bg < 8) {
  848. bg += 40;
  849. } else if (bg < 16) {
  850. bg -= 8;
  851. bg += 100;
  852. }
  853. out += bg + ';';
  854. } else {
  855. out += '48;5;' + bg + ';';
  856. }
  857. }
  858. if (fg !== 0x1ff) {
  859. fg = this._reduceColor(fg);
  860. if (fg < 16) {
  861. if (fg < 8) {
  862. fg += 30;
  863. } else if (fg < 16) {
  864. fg -= 8;
  865. fg += 90;
  866. }
  867. out += fg + ';';
  868. } else {
  869. out += '38;5;' + fg + ';';
  870. }
  871. }
  872. if (out[out.length-1] === ';') out = out.slice(0, -1);
  873. out += 'm';
  874. }
  875. }
  876. // Attempt to use ACS for supported characters.
  877. // This is not ideal, but it's how ncurses works.
  878. // There are a lot of terminals that support ACS
  879. // *and UTF8, but do not declare U8. So ACS ends
  880. // up being used (slower than utf8). Terminals
  881. // that do not support ACS and do not explicitly
  882. // support UTF8 get their unicode characters
  883. // replaced with really ugly ascii characters.
  884. // It is possible there is a terminal out there
  885. // somewhere that does not support ACS, but
  886. // supports UTF8, but I imagine it's unlikely.
  887. // Maybe remove !this.tput.unicode check, however,
  888. // this seems to be the way ncurses does it.
  889. if (this.tput.strings.enter_alt_charset_mode && !this.tput.brokenACS) {
  890. if (this.tput.acscr[ch]) {
  891. if (acs) {
  892. ch = this.tput.acscr[ch];
  893. } else {
  894. ch = this.tput.smacs()
  895. + this.tput.acscr[ch];
  896. acs = true;
  897. }
  898. } else if (acs) {
  899. ch = this.tput.rmacs() + ch;
  900. acs = false;
  901. }
  902. } else {
  903. // U8 is not consistently correct. Some terminfo's
  904. // terminals that do not declare it may actually
  905. // support utf8 (e.g. urxvt), but if the terminal
  906. // does not declare support for ACS (and U8), chances
  907. // are it does not support UTF8. This is probably
  908. // the "safest" way to do this. Should fix things
  909. // like sun-color.
  910. // if ((!this.unicode || this.tput.numbers.U8 !== 1) && ch > '~') {
  911. if ((!this.unicode || this.tput.numbers.U8 !== 1) && this.tput.utoa[ch]) {
  912. ch = this.tput.utoa[ch] || '?';
  913. }
  914. }
  915. // if (wideChars.test(ch)) x++;
  916. out += ch;
  917. attr = data;
  918. }
  919. if (attr !== this.dattr) {
  920. out += '\x1b[m';
  921. }
  922. if (out) {
  923. main += this.tput.cup(y, 0) + out;
  924. }
  925. }
  926. if (acs) {
  927. main += this.tput.rmacs();
  928. acs = false;
  929. }
  930. if (main) {
  931. pre = '';
  932. post = '';
  933. pre += this.tput.sc();
  934. post += this.tput.rc();
  935. if (!this.program.cursorHidden) {
  936. pre += this.tput.civis();
  937. post += this.tput.cnorm();
  938. }
  939. // this.program.flush();
  940. // this.program.output.write(pre + main + post);
  941. this.program._write(pre + main + post);
  942. }
  943. // this.emit('draw');
  944. };
  945. Screen.prototype._reduceColor = function(col) {
  946. if (col >= 16 && this.tput.colors <= 16) {
  947. col = colors.ccolors[col];
  948. } else if (col >= 8 && this.tput.colors <= 8) {
  949. col -= 8;
  950. } else if (col >= 2 && this.tput.colors <= 2) {
  951. col %= 2;
  952. }
  953. return col;
  954. };
  955. // Convert an SGR string to our own attribute format.
  956. Screen.prototype.attrCode = function(code, cur, def) {
  957. var flags = (cur >> 18) & 0x1ff
  958. , fg = (cur >> 9) & 0x1ff
  959. , bg = cur & 0x1ff
  960. , c
  961. , i;
  962. code = code.slice(2, -1).split(';');
  963. if (!code[0]) code[0] = '0';
  964. for (i = 0; i < code.length; i++) {
  965. c = +code[i] || 0;
  966. switch (c) {
  967. case 0: // normal
  968. //bg = 0x1ff;
  969. //fg = 0x1ff;
  970. //flags = 0;
  971. bg = def & 0x1ff;
  972. fg = (def >> 9) & 0x1ff;
  973. flags = (def >> 18) & 0x1ff;
  974. break;
  975. case 1: // bold
  976. flags |= 1;
  977. break;
  978. case 22:
  979. //flags &= ~1;
  980. flags = (def >> 18) & 0x1ff;
  981. break;
  982. case 4: // underline
  983. flags |= 2;
  984. break;
  985. case 24:
  986. //flags &= ~2;
  987. flags = (def >> 18) & 0x1ff;
  988. break;
  989. case 5: // blink
  990. flags |= 4;
  991. break;
  992. case 25:
  993. //flags &= ~4;
  994. flags = (def >> 18) & 0x1ff;
  995. break;
  996. case 7: // inverse
  997. flags |= 8;
  998. break;
  999. case 27:
  1000. //flags &= ~8;
  1001. flags = (def >> 18) & 0x1ff;
  1002. break;
  1003. case 8: // invisible
  1004. flags |= 16;
  1005. break;
  1006. case 28:
  1007. //flags &= ~16;
  1008. flags = (def >> 18) & 0x1ff;
  1009. break;
  1010. case 39: // default fg
  1011. //fg = 0x1ff;
  1012. fg = (def >> 9) & 0x1ff;
  1013. break;
  1014. case 49: // default bg
  1015. //bg = 0x1ff;
  1016. bg = def & 0x1ff;
  1017. break;
  1018. case 100: // default fg/bg
  1019. //fg = 0x1ff;
  1020. //bg = 0x1ff;
  1021. fg = (def >> 9) & 0x1ff;
  1022. bg = def & 0x1ff;
  1023. break;
  1024. default: // color
  1025. if (c === 48 && +code[i+1] === 5) {
  1026. i += 2;
  1027. bg = +code[i];
  1028. break;
  1029. } else if (c === 48 && +code[i+1] === 2) {
  1030. i += 2;
  1031. bg = colors.match(+code[i], +code[i+1], +code[i+2]);
  1032. //if (bg === -1) bg = 0x1ff;
  1033. if (bg === -1) bg = def & 0x1ff;
  1034. i += 2;
  1035. break;
  1036. } else if (c === 38 && +code[i+1] === 5) {
  1037. i += 2;
  1038. fg = +code[i];
  1039. break;
  1040. } else if (c === 38 && +code[i+1] === 2) {
  1041. i += 2;
  1042. fg = colors.match(+code[i], +code[i+1], +code[i+2]);
  1043. //if (fg === -1) fg = 0x1ff;
  1044. if (fg === -1) fg = (def >> 9) & 0x1ff;
  1045. i += 2;
  1046. break;
  1047. }
  1048. if (c >= 40 && c <= 47) {
  1049. bg = c - 40;
  1050. } else if (c >= 100 && c <= 107) {
  1051. bg = c - 100;
  1052. bg += 8;
  1053. } else if (c === 49) {
  1054. //bg = 0x1ff;
  1055. bg = def & 0x1ff;
  1056. } else if (c >= 30 && c <= 37) {
  1057. fg = c - 30;
  1058. } else if (c >= 90 && c <= 97) {
  1059. fg = c - 90;
  1060. fg += 8;
  1061. } else if (c === 39) {
  1062. //fg = 0x1ff;
  1063. fg = (def >> 9) & 0x1ff;
  1064. } else if (c === 100) {
  1065. //fg = 0x1ff;
  1066. //bg = 0x1ff;
  1067. fg = (def >> 9) & 0x1ff;
  1068. bg = def & 0x1ff;
  1069. }
  1070. break;
  1071. }
  1072. }
  1073. return (flags << 18) | (fg << 9) | bg;
  1074. };
  1075. // Convert our own attribute format to an SGR string.
  1076. Screen.prototype.codeAttr = function(code) {
  1077. var flags = (code >> 18) & 0x1ff
  1078. , fg = (code >> 9) & 0x1ff
  1079. , bg = code & 0x1ff
  1080. , out = '';
  1081. // bold
  1082. if (flags & 1) {
  1083. out += '1;';
  1084. }
  1085. // underline
  1086. if (flags & 2) {
  1087. out += '4;';
  1088. }
  1089. // blink
  1090. if (flags & 4) {
  1091. out += '5;';
  1092. }
  1093. // inverse
  1094. if (flags & 8) {
  1095. out += '7;';
  1096. }
  1097. // invisible
  1098. if (flags & 16) {
  1099. out += '8;';
  1100. }
  1101. if (bg !== 0x1ff) {
  1102. bg = this._reduceColor(bg);
  1103. if (bg < 16) {
  1104. if (bg < 8) {
  1105. bg += 40;
  1106. } else if (bg < 16) {
  1107. bg -= 8;
  1108. bg += 100;
  1109. }
  1110. out += bg + ';';
  1111. } else {
  1112. out += '48;5;' + bg + ';';
  1113. }
  1114. }
  1115. if (fg !== 0x1ff) {
  1116. fg = this._reduceColor(fg);
  1117. if (fg < 16) {
  1118. if (fg < 8) {
  1119. fg += 30;
  1120. } else if (fg < 16) {
  1121. fg -= 8;
  1122. fg += 90;
  1123. }
  1124. out += fg + ';';
  1125. } else {
  1126. out += '38;5;' + fg + ';';
  1127. }
  1128. }
  1129. if (out[out.length-1] === ';') out = out.slice(0, -1);
  1130. return '\x1b[' + out + 'm';
  1131. };
  1132. Screen.prototype.focusOffset = function(offset) {
  1133. var shown = this.keyable.filter(function(el) {
  1134. return !el.detached && el.visible;
  1135. }).length;
  1136. if (!shown || !offset) {
  1137. return;
  1138. }
  1139. var i = this.keyable.indexOf(this.focused);
  1140. if (!~i) return;
  1141. if (offset > 0) {
  1142. while (offset--) {
  1143. if (++i > this.keyable.length - 1) i = 0;
  1144. if (this.keyable[i].detached || !this.keyable[i].visible) offset++;
  1145. }
  1146. } else {
  1147. offset = -offset;
  1148. while (offset--) {
  1149. if (--i < 0) i = this.keyable.length - 1;
  1150. if (this.keyable[i].detached || !this.keyable[i].visible) offset++;
  1151. }
  1152. }
  1153. return this.keyable[i].focus();
  1154. };
  1155. Screen.prototype.focusPrev =
  1156. Screen.prototype.focusPrevious = function() {
  1157. return this.focusOffset(-1);
  1158. };
  1159. Screen.prototype.focusNext = function() {
  1160. return this.focusOffset(1);
  1161. };
  1162. Screen.prototype.focusPush = function(el) {
  1163. if (!el) return;
  1164. var old = this.history[this.history.length-1];
  1165. if (this.history.length === 10) {
  1166. this.history.shift();
  1167. }
  1168. this.history.push(el);
  1169. this._focus(el, old);
  1170. };
  1171. Screen.prototype.focusPop = function() {
  1172. var old = this.history.pop();
  1173. if (this.history.length) {
  1174. this._focus(this.history[this.history.length-1], old);
  1175. }
  1176. return old;
  1177. };
  1178. Screen.prototype.saveFocus = function() {
  1179. return this._savedFocus = this.focused;
  1180. };
  1181. Screen.prototype.restoreFocus = function() {
  1182. if (!this._savedFocus) return;
  1183. this._savedFocus.focus();
  1184. delete this._savedFocus;
  1185. return this.focused;
  1186. };
  1187. Screen.prototype.rewindFocus = function() {
  1188. var old = this.history.pop()
  1189. , el;
  1190. while (this.history.length) {
  1191. el = this.history.pop();
  1192. if (!el.detached && el.visible) {
  1193. this.history.push(el);
  1194. this._focus(el, old);
  1195. return el;
  1196. }
  1197. }
  1198. if (old) {
  1199. old.emit('blur');
  1200. }
  1201. };
  1202. Screen.prototype._focus = function(self, old) {
  1203. // Find a scrollable ancestor if we have one.
  1204. var el = self;
  1205. while (el = el.parent) {
  1206. if (el.scrollable) break;
  1207. }
  1208. // If we're in a scrollable element,
  1209. // automatically scroll to the focused element.
  1210. if (el) {
  1211. var ryi = self.top - el.top - el.itop
  1212. , ryl = self.top + self.height - el.top - el.ibottom
  1213. , visible = el.height - el.iheight;
  1214. if (ryi < el.childBase) {
  1215. el.scrollTo(ryi);
  1216. self.screen.render();
  1217. } else if (ryi >= el.childBase + visible) {
  1218. el.scrollTo(ryi + self.height);
  1219. self.screen.render();
  1220. } else if (ryl >= el.childBase + visible) {
  1221. el.scrollTo(ryi + self.height);
  1222. self.screen.render();
  1223. }
  1224. }
  1225. if (old) {
  1226. old.emit('blur', self);
  1227. }
  1228. self.emit('focus', old);
  1229. };
  1230. Screen.prototype.__defineGetter__('focused', function() {
  1231. return this.history[this.history.length-1];
  1232. });
  1233. Screen.prototype.__defineSetter__('focused', function(el) {
  1234. return this.focusPush(el);
  1235. });
  1236. Screen.prototype.clearRegion = function(xi, xl, yi, yl) {
  1237. return this.fillRegion(this.dattr, ' ', xi, xl, yi, yl);
  1238. };
  1239. Screen.prototype.fillRegion = function(attr, ch, xi, xl, yi, yl) {
  1240. var lines = this.lines
  1241. , cell
  1242. , xx;
  1243. for (; yi < yl; yi++) {
  1244. if (!lines[yi]) break;
  1245. for (xx = xi; xx < xl; xx++) {
  1246. cell = lines[yi][xx];
  1247. if (!cell) break;
  1248. if (attr !== cell[0] || ch !== cell[1]) {
  1249. lines[yi][xx][0] = attr;
  1250. lines[yi][xx][1] = ch;
  1251. lines[yi].dirty = true;
  1252. }
  1253. }
  1254. }
  1255. };
  1256. Screen.prototype.key = function() {
  1257. return this.program.key.apply(this, arguments);
  1258. };
  1259. Screen.prototype.onceKey = function() {
  1260. return this.program.onceKey.apply(this, arguments);
  1261. };
  1262. Screen.prototype.unkey =
  1263. Screen.prototype.removeKey = function() {
  1264. return this.program.unkey.apply(this, arguments);
  1265. };
  1266. Screen.prototype.spawn = function(file, args, options) {
  1267. if (!Array.isArray(args)) {
  1268. options = args;
  1269. args = [];
  1270. }
  1271. var screen = this
  1272. , program = screen.program
  1273. , options = options || {}
  1274. , spawn = require('child_process').spawn
  1275. , mouse = program.mouseEnabled
  1276. , ps;
  1277. options.stdio = 'inherit';
  1278. program.lsaveCursor('spawn');
  1279. //program.csr(0, program.rows - 1);
  1280. program.normalBuffer();
  1281. program.showCursor();
  1282. if (mouse) program.disableMouse();
  1283. var write = program.output.write;
  1284. program.output.write = function() {};
  1285. program.input.pause();
  1286. program.input.setRawMode(false);
  1287. var resume = function() {
  1288. if (resume.done) return;
  1289. resume.done = true;
  1290. program.input.setRawMode(true);
  1291. program.input.resume();
  1292. program.output.write = write;
  1293. program.alternateBuffer();
  1294. //program.csr(0, program.rows - 1);
  1295. if (mouse) program.enableMouse();
  1296. screen.alloc();
  1297. screen.render();
  1298. screen.program.lrestoreCursor('spawn', true);
  1299. };
  1300. ps = spawn(file, args, options);
  1301. ps.on('error', resume);
  1302. ps.on('exit', resume);
  1303. return ps;
  1304. };
  1305. Screen.prototype.exec = function(file, args, options, callback) {
  1306. var callback = arguments[arguments.length-1]
  1307. , ps = this.spawn(file, args, options);
  1308. ps.on('error', function(err) {
  1309. return callback(err, false);
  1310. });
  1311. ps.on('exit', function(code) {
  1312. return callback(null, code === 0);
  1313. });
  1314. return ps;
  1315. };
  1316. Screen.prototype.readEditor = function(options, callback) {
  1317. if (typeof options === 'string') {
  1318. options = { editor: options };
  1319. }
  1320. if (!callback) {
  1321. callback = options;
  1322. options = null;
  1323. }
  1324. if (!callback) {
  1325. callback = function() {};
  1326. }
  1327. options = options || {};
  1328. var self = this
  1329. , fs = require('fs')
  1330. , editor = options.editor || process.env.EDITOR || 'vi'
  1331. , name = options.name || process.title || 'blessed'
  1332. , rnd = Math.random().toString(36).split('.').pop()
  1333. , file = '/tmp/' + name + '.' + rnd
  1334. , args = [file]
  1335. , opt;
  1336. opt = {
  1337. stdio: 'inherit',
  1338. env: process.env,
  1339. cwd: process.env.HOME
  1340. };
  1341. function writeFile(callback) {
  1342. if (!options.value) return callback();
  1343. return fs.writeFile(file, options.value, callback);
  1344. }
  1345. return writeFile(function(err) {
  1346. if (err) return callback(err);
  1347. return self.exec(editor, args, opt, function(err, success) {
  1348. if (err) return callback(err);
  1349. return fs.readFile(file, 'utf8', function(err, data) {
  1350. return fs.unlink(file, function() {
  1351. if (!success) return callback(new Error('Unsuccessful.'));
  1352. if (err) return callback(err);
  1353. return callback(null, data);
  1354. });
  1355. });
  1356. });
  1357. });
  1358. };
  1359. Screen.prototype.setEffects = function(el, fel, over, out, effects, temp) {
  1360. if (!effects) return;
  1361. var tmp = {};
  1362. if (temp) el[temp] = tmp;
  1363. if (typeof el !== 'function') {
  1364. var _el = el;
  1365. el = function() { return _el; };
  1366. }
  1367. fel.on(over, function() {
  1368. var element = el();
  1369. Object.keys(effects).forEach(function(key) {
  1370. var val = effects[key];
  1371. if (val !== null && typeof val === 'object') {
  1372. tmp[key] = tmp[key] || {};
  1373. Object.keys(val).forEach(function(k) {
  1374. var v = val[k];
  1375. tmp[key][k] = element.style[key][k];
  1376. element.style[key][k] = v;
  1377. });
  1378. return;
  1379. }
  1380. tmp[key] = element.style[key];
  1381. element.style[key] = val;
  1382. });
  1383. element.screen.render();
  1384. });
  1385. fel.on(out, function() {
  1386. var element = el();
  1387. Object.keys(effects).forEach(function(key) {
  1388. var val = effects[key];
  1389. if (val !== null && typeof val === 'object') {
  1390. tmp[key] = tmp[key] || {};
  1391. Object.keys(val).forEach(function(k) {
  1392. if (tmp[key].hasOwnProperty(k)) {
  1393. element.style[key][k] = tmp[key][k];
  1394. }
  1395. });
  1396. return;
  1397. }
  1398. if (tmp.hasOwnProperty(key)) {
  1399. element.style[key] = tmp[key];
  1400. }
  1401. });
  1402. element.screen.render();
  1403. });
  1404. };
  1405. Screen.prototype.sigtstp = function(callback) {
  1406. var self = this;
  1407. this.program.sigtstp(function() {
  1408. self.alloc();
  1409. self.render();
  1410. self.program.lrestoreCursor('pause', true);
  1411. if (callback) callback();
  1412. });
  1413. };
  1414. /**
  1415. * Element
  1416. */
  1417. function Element(options) {
  1418. var self = this;
  1419. if (!(this instanceof Node)) {
  1420. return new Element(options);
  1421. }
  1422. options = options || {};
  1423. // Workaround to get a `scrollable` option.
  1424. if (options.scrollable && !this._ignore && this.type !== 'scrollable-box') {
  1425. Object.getOwnPropertyNames(ScrollableBox.prototype).forEach(function(key) {
  1426. if (key === 'type') return;
  1427. Object.defineProperty(this, key,
  1428. Object.getOwnPropertyDescriptor(ScrollableBox.prototype, key));
  1429. }, this);
  1430. this._ignore = true;
  1431. ScrollableBox.call(this, options);
  1432. delete this._ignore;
  1433. return this;
  1434. }
  1435. Node.call(this, options);
  1436. this.name = options.name;
  1437. options.position = options.position || {
  1438. left: options.left,
  1439. right: options.right,
  1440. top: options.top,
  1441. bottom: options.bottom,
  1442. width: options.width,
  1443. height: options.height
  1444. };
  1445. // if (options.position.left == null && options.position.right == null) {
  1446. // options.position.left = 0;
  1447. // }
  1448. // if (options.position.top == null && options.position.bottom == null) {
  1449. // options.position.top = 0;
  1450. // }
  1451. if (options.position.width === 'shrink'
  1452. || options.position.height === 'shrink') {
  1453. if (options.position.width === 'shrink') {
  1454. delete options.position.width;
  1455. }
  1456. if (options.position.height === 'shrink') {
  1457. delete options.position.height;
  1458. }
  1459. options.shrink = true;
  1460. }
  1461. this.position = options.position;
  1462. this.noOverflow = options.noOverflow;
  1463. this.style = options.style;
  1464. if (!this.style) {
  1465. this.style = {};
  1466. this.style.fg = options.fg;
  1467. this.style.bg = options.bg;
  1468. this.style.bold = options.bold;
  1469. this.style.underline = options.underline;
  1470. this.style.blink = options.blink;
  1471. this.style.inverse = options.inverse;
  1472. this.style.invisible = options.invisible;
  1473. }
  1474. this.hidden = options.hidden || false;
  1475. this.fixed = options.fixed || false;
  1476. this.align = options.align || 'left';
  1477. this.valign = options.valign || 'top';
  1478. this.wrap = options.wrap !== false;
  1479. this.shrink = options.shrink;
  1480. this.fixed = options.fixed;
  1481. this.ch = options.ch || ' ';
  1482. if (typeof options.padding === 'number' || !options.padding) {
  1483. options.padding = {
  1484. left: options.padding,
  1485. top: options.padding,
  1486. right: options.padding,
  1487. bottom: options.padding
  1488. };
  1489. }
  1490. this.padding = {
  1491. left: options.padding.left || 0,
  1492. top: options.padding.top || 0,
  1493. right: options.padding.right || 0,
  1494. bottom: options.padding.bottom || 0
  1495. };
  1496. this.border = options.border;
  1497. if (this.border) {
  1498. if (typeof this.border === 'string') {
  1499. this.border = { type: this.border };
  1500. }
  1501. this.border.type = this.border.type || 'bg';
  1502. if (this.border.type === 'ascii') this.border.type = 'line';
  1503. this.border.ch = this.border.ch || ' ';
  1504. if (!this.border.style) {
  1505. this.border.style = this.style.border || {};
  1506. this.border.style.fg = this.border.fg;
  1507. this.border.style.bg = this.border.bg;
  1508. }
  1509. this.style.border = this.border.style;
  1510. }
  1511. if (options.clickable) {
  1512. this.screen._listenMouse(this);
  1513. }
  1514. if (options.input || options.keyable) {
  1515. this.screen._listenKeys(this);
  1516. }
  1517. this.parseTags = options.parseTags || options.tags;
  1518. this.setContent(options.content || '', true);
  1519. if (options.label) {
  1520. this.setLabel(options.label);
  1521. }
  1522. // TODO: Possibly move this to Node for screen.on('mouse', ...).
  1523. this.on('newListener', function fn(type) {
  1524. //type = type.split(' ').slice(1).join(' ');
  1525. if (type === 'mouse'
  1526. || type === 'click'
  1527. || type === 'mouseover'
  1528. || type === 'mouseout'
  1529. || type === 'mousedown'
  1530. || type === 'mouseup'
  1531. || type === 'mousewheel'
  1532. || type === 'wheeldown'
  1533. || type === 'wheelup'
  1534. || type === 'mousemove') {
  1535. self.screen._listenMouse(self);
  1536. } else if (type === 'keypress' || type.indexOf('key ') === 0) {
  1537. self.screen._listenKeys(self);
  1538. }
  1539. });
  1540. this.on('resize', function() {
  1541. self.parseContent();
  1542. });
  1543. this.on('attach', function() {
  1544. self.parseContent();
  1545. });
  1546. this.on('detach', function() {
  1547. delete self.lpos;
  1548. });
  1549. if (options.hoverBg != null) {
  1550. options.hoverEffects = options.hoverEffects || {};
  1551. options.hoverEffects.bg = options.hoverBg;
  1552. }
  1553. if (this.style.hover) {
  1554. options.hoverEffects = this.style.hover;
  1555. //delete this.style.hover;
  1556. }
  1557. if (this.style.focus) {
  1558. options.focusEffects = this.style.focus;
  1559. //delete this.style.focus;
  1560. }
  1561. // if (options.effects) {
  1562. // if (options.effects.hover) options.hoverEffects = options.effects.hover;
  1563. // if (options.effects.focus) options.focusEffects = options.effects.focus;
  1564. // }
  1565. [['hoverEffects', 'mouseover', 'mouseout', '_htemp'],
  1566. ['focusEffects', 'focus', 'blur', '_ftemp']].forEach(function(props) {
  1567. var pname = props[0], over = props[1], out = props[2], temp = props[3];
  1568. self.screen.setEffects(self, self, over, out, self.options[pname], temp);
  1569. });
  1570. if (options.focused) {
  1571. this.focus();
  1572. }
  1573. }
  1574. Element.prototype.__proto__ = Node.prototype;
  1575. Element.prototype.type = 'element';
  1576. Element.prototype.__defineGetter__('focused', function() {
  1577. return this.screen.focused === this;
  1578. });
  1579. Element.prototype.sattr = function(obj, fg, bg) {
  1580. var bold = obj.bold
  1581. , underline = obj.underline
  1582. , blink = obj.blink
  1583. , inverse = obj.inverse
  1584. , invisible = obj.invisible;
  1585. // This used to be a loop, but I decided
  1586. // to unroll it for performance's sake.
  1587. if (typeof bold === 'function') bold = bold(this);
  1588. if (typeof underline === 'function') underline = underline(this);
  1589. if (typeof blink === 'function') blink = blink(this);
  1590. if (typeof inverse === 'function') inverse = inverse(this);
  1591. if (typeof invisible === 'function') invisible = invisible(this);
  1592. if (typeof fg === 'function') fg = fg(this);
  1593. if (typeof bg === 'function') bg = bg(this);
  1594. return ((((invisible ? 16 : 0) << 18)
  1595. | ((inverse ? 8 : 0) << 18)
  1596. | ((blink ? 4 : 0) << 18)
  1597. | ((underline ? 2 : 0) << 18))
  1598. | ((bold ? 1 : 0) << 18)
  1599. | (colors.convert(fg) << 9))
  1600. | colors.convert(bg);
  1601. };
  1602. Element.prototype.onScreenEvent = function(type, listener) {
  1603. var self = this;
  1604. if (this.parent) {
  1605. this.screen.on(type, listener);
  1606. }
  1607. this.on('attach', function() {
  1608. self.screen.on(type, listener);
  1609. });
  1610. this.on('detach', function() {
  1611. self.screen.removeListener(type, listener);
  1612. });
  1613. };
  1614. Element.prototype.hide = function() {
  1615. if (this.hidden) return;
  1616. this.clearPos();
  1617. this.hidden = true;
  1618. this.emit('hide');
  1619. if (this.screen.focused === this) {
  1620. this.screen.rewindFocus();
  1621. }
  1622. };
  1623. Element.prototype.show = function() {
  1624. if (!this.hidden) return;
  1625. this.hidden = false;
  1626. this.emit('show');
  1627. };
  1628. Element.prototype.toggle = function() {
  1629. return this.hidden ? this.show() : this.hide();
  1630. };
  1631. Element.prototype.focus = function() {
  1632. return this.screen.focused = this;
  1633. };
  1634. Element.prototype.setContent = function(content, noClear, noTags) {
  1635. if (!noClear) this.clearPos();
  1636. this.content = content || '';
  1637. this.parseContent(noTags);
  1638. };
  1639. Element.prototype.getContent = function() {
  1640. return this._clines.fake.join('\n');
  1641. };
  1642. Element.prototype.setText = function(content, noClear) {
  1643. content = content || '';
  1644. content = content.replace(/\x1b\[[\d;]*m/g, '');
  1645. return this.setContent(content, noClear, true);
  1646. };
  1647. Element.prototype.getText = function() {
  1648. return this.getContent().replace(/\x1b\[[\d;]*m/g, '');
  1649. };
  1650. Element.prototype.parseContent = function(noTags) {
  1651. if (this.detached) return false;
  1652. var width = this.width - this.iwidth;
  1653. if (this._clines == null
  1654. || this._clines.width !== width
  1655. || this._clines.content !== this.content) {
  1656. var content = this.content;
  1657. content = content
  1658. .replace(/[\x00-\x08\x0b-\x0c\x0e-\x1a\x1c-\x1f\x7f]/g, '')
  1659. .replace(/\x1b(?!\[[\d;]*m)/g, '')
  1660. .replace(/\r\n|\r/g, '\n')
  1661. .replace(/\t/g, this.screen.tabc)
  1662. .replace(wideChars, '?');
  1663. if (!noTags) {
  1664. content = this._parseTags(content);
  1665. }
  1666. this._clines = this._wrapContent(content, width);
  1667. this._clines.width = width;
  1668. this._clines.content = this.content;
  1669. this._clines.attr = this._parseAttr(this._clines);
  1670. this._clines.ci = [];
  1671. this._clines.reduce(function(total, line) {
  1672. this._clines.ci.push(total);
  1673. return total + line.length + 1;
  1674. }.bind(this), 0);
  1675. this._pcontent = this._clines.join('\n');
  1676. this.emit('parsed content');
  1677. return true;
  1678. }
  1679. // Need to calculate this every time because the default fg/bg may change.
  1680. this._clines.attr = this._parseAttr(this._clines) || this._clines.attr;
  1681. return false;
  1682. };
  1683. // Convert `{red-fg}foo{/red-fg}` to `\x1b[31mfoo\x1b[39m`.
  1684. Element.prototype._parseTags = function(text) {
  1685. if (!this.parseTags) return text;
  1686. if (!/{\/?[\w\-,;!#]*}/.test(text)) return text;
  1687. var program = this.screen.program
  1688. , out = ''
  1689. , state
  1690. , bg = []
  1691. , fg = []
  1692. , flag = []
  1693. , cap
  1694. , slash
  1695. , param
  1696. , attr
  1697. , esc;
  1698. for (;;) {
  1699. if (!esc && (cap = /^{escape}/.exec(text))) {
  1700. text = text.substring(cap[0].length);
  1701. esc = true;
  1702. continue;
  1703. }
  1704. if (esc && (cap = /^([\s\S]+?){\/escape}/.exec(text))) {
  1705. text = text.substring(cap[0].length);
  1706. out += cap[1];
  1707. esc = false;
  1708. continue;
  1709. }
  1710. if (esc) {
  1711. // throw new Error('Unterminated escape tag.');
  1712. out += text;
  1713. break;
  1714. }
  1715. if (cap = /^{(\/?)([\w\-,;!#]*)}/.exec(text)) {
  1716. text = text.substring(cap[0].length);
  1717. slash = cap[1] === '/';
  1718. param = cap[2].replace(/-/g, ' ');
  1719. if (param === 'open') {
  1720. out += '{';
  1721. continue;
  1722. } else if (param === 'close') {
  1723. out += '}';
  1724. continue;
  1725. }
  1726. if (param.slice(-3) === ' bg') state = bg;
  1727. else if (param.slice(-3) === ' fg') state = fg;
  1728. else state = flag;
  1729. if (slash) {
  1730. if (!param) {
  1731. out += program._attr('normal');
  1732. bg.length = 0;
  1733. fg.length = 0;
  1734. flag.length = 0;
  1735. } else {
  1736. attr = program._attr(p

Large files files are truncated, but you can click here to view the full file