PageRenderTime 40ms 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
  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(param, false);
  1737. if (attr == null) {
  1738. out += cap[0];
  1739. } else {
  1740. // if (param !== state[state.length-1]) {
  1741. // throw new Error('Misnested tags.');
  1742. // }
  1743. state.pop();
  1744. if (state.length) {
  1745. out += program._attr(state[state.length-1]);
  1746. } else {
  1747. out += attr;
  1748. }
  1749. }
  1750. }
  1751. } else {
  1752. if (!param) {
  1753. out += cap[0];
  1754. } else {
  1755. attr = program._attr(param);
  1756. if (attr == null) {
  1757. out += cap[0];
  1758. } else {
  1759. state.push(param);
  1760. out += attr;
  1761. }
  1762. }
  1763. }
  1764. continue;
  1765. }
  1766. if (cap = /^[\s\S]+?(?={\/?[\w\-,;!#]*})/.exec(text)) {
  1767. text = text.substring(cap[0].length);
  1768. out += cap[0];
  1769. continue;
  1770. }
  1771. out += text;
  1772. break;
  1773. }
  1774. return out;
  1775. };
  1776. Element.prototype._parseAttr = function(lines) {
  1777. var dattr = this.sattr(this.style, this.style.fg, this.style.bg)
  1778. , attr = dattr
  1779. , attrs = []
  1780. , line
  1781. , i
  1782. , j
  1783. , c;
  1784. if (lines[0].attr === attr) {
  1785. return;
  1786. }
  1787. for (j = 0; j < lines.length; j++) {
  1788. line = lines[j];
  1789. attrs[j] = attr;
  1790. for (i = 0; i < line.length; i++) {
  1791. if (line[i] === '\x1b') {
  1792. if (c = /^\x1b\[[\d;]*m/.exec(line.substring(i))) {
  1793. attr = this.screen.attrCode(c[0], attr, dattr);
  1794. i += c[0].length - 1;
  1795. }
  1796. }
  1797. }
  1798. }
  1799. return attrs;
  1800. };
  1801. Element.prototype._align = function(line, width, align) {
  1802. if (!align) return line;
  1803. var len = line.replace(/\x1b\[[\d;]*m/g, '').length
  1804. , s = width - len;
  1805. if (len === 0) return line;
  1806. if (s < 0) return line;
  1807. if (align === 'center') {
  1808. s = Array(((s / 2) | 0) + 1).join(' ');
  1809. return s + line + s;
  1810. } else if (align === 'right') {
  1811. s = Array(s + 1).join(' ');
  1812. return s + line;
  1813. }
  1814. return line;
  1815. };
  1816. Element.prototype._wrapContent = function(content, width) {
  1817. var tags = this.parseTags
  1818. , state = this.align
  1819. , wrap = this.wrap
  1820. , margin = 0
  1821. , rtof = []
  1822. , ftor = []
  1823. , fake = []
  1824. , out = []
  1825. , no = 0
  1826. , line
  1827. , align
  1828. , cap
  1829. , total
  1830. , i
  1831. , part
  1832. , j
  1833. , lines
  1834. , rest;
  1835. lines = content.split('\n');
  1836. if (!content) {
  1837. out.push(content);
  1838. out.rtof = [0];
  1839. out.ftor = [[0]];
  1840. out.fake = lines;
  1841. out.real = out;
  1842. out.mwidth = 0;
  1843. return out;
  1844. }
  1845. if (this.scrollbar) margin++;
  1846. if (this.type === 'textarea') margin++;
  1847. if (width > margin) width -= margin;
  1848. main:
  1849. for (; no < lines.length; no++) {
  1850. line = lines[no];
  1851. align = state;
  1852. ftor.push([]);
  1853. // Handle alignment tags.
  1854. if (tags) {
  1855. if (cap = /^{(left|center|right)}/.exec(line)) {
  1856. line = line.substring(cap[0].length);
  1857. align = state = cap[1] !== 'left'
  1858. ? cap[1]
  1859. : null;
  1860. }
  1861. if (cap = /{\/(left|center|right)}$/.exec(line)) {
  1862. line = line.slice(0, -cap[0].length);
  1863. state = null;
  1864. }
  1865. }
  1866. // If the string is apparently too long, wrap it.
  1867. while (line.length > width) {
  1868. // Measure the real width of the string.
  1869. for (i = 0, total = 0; i < line.length; i++) {
  1870. while (line[i] === '\x1b') {
  1871. while (line[i] && line[i++] !== 'm');
  1872. }
  1873. if (!line[i]) break;
  1874. if (++total === width) {
  1875. // If we're not wrapping the text, we have to finish up the rest of
  1876. // the control sequences before cutting off the line.
  1877. i++;
  1878. if (!wrap) {
  1879. rest = line.substring(i).match(/\x1b\[[^m]*m/g);
  1880. rest = rest ? rest.join('') : '';
  1881. out.push(this._align(line.substring(0, i) + rest, width, align));
  1882. ftor[no].push(out.length - 1);
  1883. rtof.push(no);
  1884. continue main;
  1885. }
  1886. // Try to find a space to break on.
  1887. if (i !== line.length) {
  1888. j = i;
  1889. while (j > i - 10 && j > 0 && line[--j] !== ' ');
  1890. if (line[j] === ' ') i = j + 1;
  1891. }
  1892. break;
  1893. }
  1894. }
  1895. part = line.substring(0, i);
  1896. line = line.substring(i);
  1897. out.push(this._align(part, width, align));
  1898. ftor[no].push(out.length - 1);
  1899. rtof.push(no);
  1900. // Make sure we didn't wrap the line to the very end, otherwise
  1901. // we get a pointless empty line after a newline.
  1902. if (line === '') continue main;
  1903. // If only an escape code got cut off, at it to `part`.
  1904. if (/^(?:\x1b[\[\d;]*m)+$/.test(line)) {
  1905. out[out.length-1] += line;
  1906. continue main;
  1907. }
  1908. }
  1909. out.push(this._align(line, width, align));
  1910. ftor[no].push(out.length - 1);
  1911. rtof.push(no);
  1912. }
  1913. out.rtof = rtof;
  1914. out.ftor = ftor;
  1915. out.fake = lines;
  1916. out.real = out;
  1917. out.mwidth = out.reduce(function(current, line) {
  1918. line = line.replace(/\x1b\[[\d;]*m/g, '');
  1919. return line.length > current
  1920. ? line.length
  1921. : current;
  1922. }, 0);
  1923. return out;
  1924. };
  1925. Element.prototype.__defineGetter__('visible', function() {
  1926. var el = this;
  1927. do {
  1928. if (el.detached) return false;
  1929. if (el.hidden) return false;
  1930. } while (el = el.parent);
  1931. return true;
  1932. });
  1933. Element.prototype.__defineGetter__('_detached', function() {
  1934. var el = this;
  1935. do {
  1936. if (el.type === 'screen') return false;
  1937. if (!el.parent) return true;
  1938. } while (el = el.parent);
  1939. return false;
  1940. });
  1941. Element.prototype.key = function() {
  1942. return this.screen.program.key.apply(this, arguments);
  1943. };
  1944. Element.prototype.onceKey = function() {
  1945. return this.screen.program.onceKey.apply(this, arguments);
  1946. };
  1947. Element.prototype.unkey =
  1948. Element.prototype.removeKey = function() {
  1949. return this.screen.program.unkey.apply(this, arguments);
  1950. };
  1951. Element.prototype.setIndex = function(index) {
  1952. if (!this.parent) return;
  1953. if (index < 0) {
  1954. index = this.parent.children.length + index;
  1955. }
  1956. index = Math.max(index, 0);
  1957. index = Math.min(index, this.parent.children.length - 1);
  1958. var i = this.parent.children.indexOf(this);
  1959. if (!~i) return;
  1960. var item = this.parent.children.splice(i, 1)[0]
  1961. this.parent.children.splice(index, 0, item);
  1962. };
  1963. Element.prototype.setFront = function() {
  1964. return this.setIndex(-1);
  1965. };
  1966. Element.prototype.setBack = function() {
  1967. return this.setIndex(0);
  1968. };
  1969. Element.prototype.clearPos = function(get) {
  1970. if (this.detached) return;
  1971. var lpos = this._getCoords(get);
  1972. if (!lpos) return;
  1973. this.screen.clearRegion(
  1974. lpos.xi, lpos.xl,
  1975. lpos.yi, lpos.yl);
  1976. };
  1977. Element.prototype.setLabel = function(text) {
  1978. if (this._label) {
  1979. if (text) {
  1980. this._label.setContent(text);
  1981. } else {
  1982. this._label.detach();
  1983. delete this._label;
  1984. }
  1985. return;
  1986. }
  1987. this._label = new Box({
  1988. screen: this.screen,
  1989. parent: this,
  1990. content: text,
  1991. left: 2,
  1992. top: this.border ? 0 : -1,
  1993. tags: this.parseTags,
  1994. shrink: true,
  1995. style: this.style.label,
  1996. fixed: true
  1997. });
  1998. if (this.screen.autoPadding) {
  1999. this._label.rleft = -this.ileft + 2;
  2000. this._label.rtop = -this.itop;
  2001. }
  2002. };
  2003. Element.prototype.removeLabel = function() {
  2004. return this.setLabel(null);
  2005. };
  2006. /**
  2007. * Positioning
  2008. */
  2009. // The below methods are a bit confusing: basically
  2010. // whenever Box.render is called `lpos` gets set on
  2011. // the element, an object containing the rendered
  2012. // coordinates. Since these don't update if the
  2013. // element is moved somehow, they're unreliable in
  2014. // that situation. However, if we can guarantee that
  2015. // lpos is good and up to date, it can be more
  2016. // accurate than the calculated positions below.
  2017. // In this case, if the element is being rendered,
  2018. // it's guaranteed that the parent will have been
  2019. // rendered first, in which case we can use the
  2020. // parant's lpos instead of recalculating it's
  2021. // position (since that might be wrong because
  2022. // it doesn't handle content shrinkage).
  2023. Screen.prototype._getPos = function() {
  2024. return this;
  2025. };
  2026. Element.prototype._getPos = function() {
  2027. var pos = this.lpos;
  2028. assert.ok(pos);
  2029. if (pos.left != null) return pos;
  2030. pos.left = pos.xi;
  2031. pos.top = pos.yi;
  2032. pos.right = this.screen.cols - pos.xl;
  2033. pos.bottom = this.screen.rows - pos.yl;
  2034. pos.width = pos.xl - pos.xi;
  2035. pos.height = pos.yl - pos.yi;
  2036. return pos;
  2037. };
  2038. /**
  2039. * Position Getters
  2040. */
  2041. Element.prototype._getWidth = function(get) {
  2042. var parent = get ? this.parent._getPos() : this.parent
  2043. , width = this.position.width || 0
  2044. , left;
  2045. if (typeof width === 'string') {
  2046. if (width === 'half') width = '50%';
  2047. width = +width.slice(0, -1) / 100;
  2048. return parent.width * width | 0;
  2049. }
  2050. // This is for if the element is being streched or shrunken.
  2051. // Although the width for shrunken elements is calculated
  2052. // in the render function, it may be calculated based on
  2053. // the content width, and the content width is initially
  2054. // decided by the width the element, so it needs to be
  2055. // calculated here.
  2056. if (!width) {
  2057. left = this.position.left || 0;
  2058. if (typeof left === 'string') {
  2059. if (left === 'center') left = '50%';
  2060. left = +left.slice(0, -1) / 100;
  2061. left = parent.width * left | 0;
  2062. }
  2063. width = parent.width - (this.position.right || 0) - left;
  2064. if (this.screen.autoPadding) {
  2065. if ((this.position.left != null || this.position.right == null)
  2066. && this.position.left !== 'center') {
  2067. width -= this.parent.ileft;
  2068. }
  2069. //if (this.position.right != null) {
  2070. width -= this.parent.iright;
  2071. //}
  2072. }
  2073. }
  2074. return width;
  2075. };
  2076. Element.prototype.__defineGetter__('width', function() {
  2077. return this._getWidth(false);
  2078. });
  2079. Element.prototype._getHeight = function(get) {
  2080. var parent = get ? this.parent._getPos() : this.parent
  2081. , height = this.position.height || 0
  2082. , top;
  2083. if (typeof height === 'string') {
  2084. if (height === 'half') height = '50%';
  2085. height = +height.slice(0, -1) / 100;
  2086. return parent.height * height | 0;
  2087. }
  2088. // This is for if the element is being streched or shrunken.
  2089. // Although the width for shrunken elements is calculated
  2090. // in the render function, it may be calculated based on
  2091. // the content width, and the content width is initially
  2092. // decided by the width the element, so it needs to be
  2093. // calculated here.
  2094. if (!height) {
  2095. top = this.position.top || 0;
  2096. if (typeof top === 'string') {
  2097. if (top === 'center') top = '50%';
  2098. top = +top.slice(0, -1) / 100;
  2099. top = parent.height * top | 0;
  2100. }
  2101. height = parent.height - (this.position.bottom || 0) - top;
  2102. if (this.screen.autoPadding) {
  2103. if ((this.position.top != null
  2104. || this.position.bottom == null)
  2105. && this.position.top !== 'center') {
  2106. height -= this.parent.itop;
  2107. }
  2108. //if (this.position.bottom != null) {
  2109. height -= this.parent.ibottom;
  2110. //}
  2111. }
  2112. }
  2113. return height;
  2114. };
  2115. Element.prototype.__defineGetter__('height', function() {
  2116. return this._getHeight(false);
  2117. });
  2118. Element.prototype._getLeft = function(get) {
  2119. var parent = get ? this.parent._getPos() : this.parent
  2120. , left = this.position.left || 0;
  2121. if (typeof left === 'string') {
  2122. if (left === 'center') left = '50%';
  2123. left = +left.slice(0, -1) / 100;
  2124. left = parent.width * left | 0;
  2125. if (this.position.left === 'center') {
  2126. left -= this._getWidth(get) / 2 | 0;
  2127. }
  2128. }
  2129. if (this.position.left == null && this.position.right != null) {
  2130. return this.screen.cols - this._getWidth(get) - this._getRight(get);
  2131. }
  2132. if (this.screen.autoPadding) {
  2133. if ((this.position.left != null
  2134. || this.position.right == null)
  2135. && this.position.left !== 'center') {
  2136. left += this.parent.ileft;
  2137. }
  2138. }
  2139. return (parent.left || 0) + left;
  2140. };
  2141. Element.prototype.__defineGetter__('left', function() {
  2142. return this._getLeft(false);
  2143. });
  2144. Element.prototype._getRight = function(get) {
  2145. var parent = get ? this.parent._getPos() : this.parent
  2146. , right;
  2147. if (this.position.right == null && this.position.left != null) {
  2148. right = this.screen.cols - (this._getLeft(get) + this._getWidth(get));
  2149. if (this.screen.autoPadding) {
  2150. //if (this.position.right != null) {
  2151. right += this.parent.iright;
  2152. //}
  2153. }
  2154. return right;
  2155. }
  2156. right = (parent.right || 0) + (this.position.right || 0);
  2157. if (this.screen.autoPadding) {
  2158. //if (this.position.right != null) {
  2159. right += this.parent.iright;
  2160. //}
  2161. }
  2162. return right;
  2163. };
  2164. Element.prototype.__defineGetter__('right', function() {
  2165. return this._getRight(false);
  2166. });
  2167. Element.prototype._getTop = function(get) {
  2168. var parent = get ? this.parent._getPos() : this.parent
  2169. , top = this.position.top || 0;
  2170. if (typeof top === 'string') {
  2171. if (top === 'center') top = '50%';
  2172. top = +top.slice(0, -1) / 100;
  2173. top = parent.height * top | 0;
  2174. if (this.position.top === 'center') {
  2175. top -= this._getHeight(get) / 2 | 0;
  2176. }
  2177. }
  2178. if (this.position.top == null && this.position.bottom != null) {
  2179. return this.screen.rows - this._getHeight(get) - this._getBottom(get);
  2180. }
  2181. if (this.screen.autoPadding) {
  2182. if ((this.position.top != null
  2183. || this.position.bottom == null)
  2184. && this.position.top !== 'center') {
  2185. top += this.parent.itop;
  2186. }
  2187. }
  2188. return (parent.top || 0) + top;
  2189. };
  2190. Element.prototype.__defineGetter__('top', function() {
  2191. return this._getTop(false);
  2192. });
  2193. Element.prototype._getBottom = function(get) {
  2194. var parent = get ? this.parent._getPos() : this.parent
  2195. , bottom;
  2196. if (this.position.bottom == null && this.position.top != null) {
  2197. bottom = this.screen.rows - (this._getTop(get) + this._getHeight(get));
  2198. if (this.screen.autoPadding) {
  2199. //if (this.position.bottom != null) {
  2200. bottom += this.parent.ibottom;
  2201. //}
  2202. }
  2203. return bottom;
  2204. }
  2205. bottom = (parent.bottom || 0) + (this.position.bottom || 0);
  2206. if (this.screen.autoPadding) {
  2207. //if (this.position.bottom != null) {
  2208. bottom += this.parent.ibottom;
  2209. //}
  2210. }
  2211. return bottom;
  2212. };
  2213. Element.prototype.__defineGetter__('bottom', function() {
  2214. return this._getBottom(false);
  2215. });
  2216. Element.prototype.__defineGetter__('rleft', function() {
  2217. return this.left - this.parent.left;
  2218. });
  2219. Element.prototype.__defineGetter__('rright', function() {
  2220. return this.right - this.parent.right;
  2221. });
  2222. Element.prototype.__defineGetter__('rtop', function() {
  2223. return this.top - this.parent.top;
  2224. });
  2225. Element.prototype.__defineGetter__('rbottom', function() {
  2226. return this.bottom - this.parent.bottom;
  2227. });
  2228. /**
  2229. * Position Setters
  2230. */
  2231. // NOTE:
  2232. // For right, bottom, rright, and rbottom:
  2233. // If position.bottom is null, we could simply set top instead.
  2234. // But it wouldn't replicate bottom behavior appropriately if
  2235. // the parent was resized, etc.
  2236. Element.prototype.__defineSetter__('width', function(val) {
  2237. if (this.position.width === val) return;
  2238. this.emit('resize');
  2239. this.clearPos();
  2240. return this.position.width = val;
  2241. });
  2242. Element.prototype.__defineSetter__('height', function(val) {
  2243. if (this.position.height === val) return;
  2244. this.emit('resize');
  2245. this.clearPos();
  2246. return this.position.height = val;
  2247. });
  2248. Element.prototype.__defineSetter__('left', function(val) {
  2249. if (typeof val === 'string') {
  2250. if (val === 'center') {
  2251. val = this.screen.width / 2 | 0;
  2252. val -= this.width / 2 | 0;
  2253. } else {
  2254. val = +val.slice(0, -1) / 100;
  2255. val = this.screen.width * val | 0;
  2256. }
  2257. }
  2258. val -= this.parent.left;
  2259. if (this.position.left === val) return;
  2260. this.emit('move');
  2261. this.clearPos();
  2262. return this.position.left = val;
  2263. });
  2264. Element.prototype.__defineSetter__('right', function(val) {
  2265. val -= this.parent.right;
  2266. if (this.position.right === val) return;
  2267. this.emit('move');
  2268. this.clearPos();
  2269. return this.position.right = val;
  2270. });
  2271. Element.prototype.__defineSetter__('top', function(val) {
  2272. if (typeof val === 'string') {
  2273. if (val === 'center') {
  2274. val = this.screen.height / 2 | 0;
  2275. val -= this.height / 2 | 0;
  2276. } else {
  2277. val = +val.slice(0, -1) / 100;
  2278. val = this.screen.height * val | 0;
  2279. }
  2280. }
  2281. val -= this.parent.top;
  2282. if (this.position.top === val) return;
  2283. this.emit('move');
  2284. this.clearPos();
  2285. return this.position.top = val;
  2286. });
  2287. Element.prototype.__defineSetter__('bottom', function(val) {
  2288. val -= this.parent.bottom;
  2289. if (this.position.bottom === val) return;
  2290. this.emit('move');
  2291. this.clearPos();
  2292. return this.position.bottom = val;
  2293. });
  2294. Element.prototype.__defineSetter__('rleft', function(val) {
  2295. if (this.position.left === val) return;
  2296. this.emit('move');
  2297. this.clearPos();
  2298. return this.position.left = val;
  2299. });
  2300. Element.prototype.__defineSetter__('rright', function(val) {
  2301. if (this.position.right === val) return;
  2302. this.emit('move');
  2303. this.clearPos();
  2304. return this.position.right = val;
  2305. });
  2306. Element.prototype.__defineSetter__('rtop', function(val) {
  2307. if (this.position.top === val) return;
  2308. this.emit('move');
  2309. this.clearPos();
  2310. return this.position.top = val;
  2311. });
  2312. Element.prototype.__defineSetter__('rbottom', function(val) {
  2313. if (this.position.bottom === val) return;
  2314. this.emit('move');
  2315. this.clearPos();
  2316. return this.position.bottom = val;
  2317. });
  2318. Element.prototype.__defineGetter__('ileft', function() {
  2319. return (this.border ? 1 : 0) + this.padding.left;
  2320. });
  2321. Element.prototype.__defineGetter__('itop', function() {
  2322. return (this.border ? 1 : 0) + this.padding.top;
  2323. });
  2324. Element.prototype.__defineGetter__('iright', function() {
  2325. return (this.border ? 1 : 0) + this.padding.right;
  2326. });
  2327. Element.prototype.__defineGetter__('ibottom', function() {
  2328. return (this.border ? 1 : 0) + this.padding.bottom;
  2329. });
  2330. Element.prototype.__defineGetter__('iwidth', function() {
  2331. return (this.border ? 2 : 0) + this.padding.left + this.padding.right;
  2332. });
  2333. Element.prototype.__defineGetter__('iheight', function() {
  2334. return (this.border ? 2 : 0) + this.padding.top + this.padding.bottom;
  2335. });
  2336. Element.prototype.__defineGetter__('tpadding', function() {
  2337. return this.padding.left + this.padding.top
  2338. + this.padding.right + this.padding.bottom;
  2339. });
  2340. /**
  2341. * Rendering - here be dragons
  2342. */
  2343. Element.prototype._getShrinkBox = function(xi, xl, yi, yl, get) {
  2344. if (!this.children.length) {
  2345. return { xi: xi, xl: xi + 1, yi: yi, yl: yi + 1 };
  2346. }
  2347. var i, el, ret, mxi = xi, mxl = xi + 1, myi = yi, myl = yi + 1;
  2348. // This is a chicken and egg problem. We need to determine how the children
  2349. // will render in order to determine how this element renders, but it in
  2350. // order to figure out how the children will render, they need to know
  2351. // exactly how their parent renders, so, we can give them what we have so
  2352. // far.
  2353. var _lpos;
  2354. if (get) {
  2355. _lpos = this.lpos;
  2356. this.lpos = { xi: xi, xl: xl, yi: yi, yl: yl };
  2357. //this.shrink = false;
  2358. }
  2359. for (i = 0; i < this.children.length; i++) {
  2360. el = this.children[i];
  2361. ret = el._getCoords(get);
  2362. // Or just (seemed to work, but probably not good):
  2363. // ret = el.lpos || this.lpos;
  2364. if (!ret) continue;
  2365. // Since the parent element is shrunk, and the child elements think it's
  2366. // going to take up as much space as possible, an element anchored to the
  2367. // right or bottom will inadvertantly make the parent's shrunken size as
  2368. // large as possible. So, we can just use the height and/or width the of
  2369. // element.
  2370. // if (get) {
  2371. if (el.position.left == null && el.position.right != null) {
  2372. ret.xl = xi + (ret.xl - ret.xi);
  2373. ret.xi = xi;
  2374. if (this.screen.autoPadding) {
  2375. // Maybe just do this no matter what.
  2376. ret.xl += this.ileft;
  2377. ret.xi += this.ileft;
  2378. }
  2379. }
  2380. if (el.position.top == null && el.position.bottom != null) {
  2381. ret.yl = yi + (ret.yl - ret.yi);
  2382. ret.yi = yi;
  2383. if (this.screen.autoPadding) {
  2384. // Maybe just do this no matter what.
  2385. ret.yl += this.itop;
  2386. ret.yi += this.itop;
  2387. }
  2388. }
  2389. if (ret.xi < mxi) mxi = ret.xi;
  2390. if (ret.xl > mxl) mxl = ret.xl;
  2391. if (ret.yi < myi) myi = ret.yi;
  2392. if (ret.yl > myl) myl = ret.yl;
  2393. }
  2394. if (get) {
  2395. this.lpos = _lpos;
  2396. //this.shrink = true;
  2397. }
  2398. if (this.position.width == null
  2399. && (this.position.left == null
  2400. || this.position.right == null)) {
  2401. if (this.position.left == null && this.position.right != null) {
  2402. xi = xl - (mxl - mxi);
  2403. if (!this.screen.autoPadding) {
  2404. xi -= this.padding.left + this.padding.right;
  2405. } else {
  2406. //xi -= this.padding.left;
  2407. xi -= this.ileft;
  2408. }
  2409. } else {
  2410. xl = mxl;
  2411. if (!this.screen.autoPadding) {
  2412. xl += this.padding.left + this.padding.right;
  2413. } else {
  2414. //xl += this.padding.right;
  2415. xl += this.iright;
  2416. }
  2417. }
  2418. }
  2419. if (this.position.height == null
  2420. && (this.position.top == null
  2421. || this.position.bottom == null)
  2422. && !this.scrollable) {
  2423. if (this.position.top == null && this.position.bottom != null) {
  2424. yi = yl - (myl - myi);
  2425. if (!this.screen.autoPadding) {
  2426. yi -= this.padding.top + this.padding.bottom;
  2427. } else {
  2428. //yi -= this.padding.top;
  2429. yi -= this.itop;
  2430. }
  2431. } else {
  2432. yl = myl;
  2433. if (!this.screen.autoPadding) {
  2434. yl += this.padding.top + this.padding.bottom;
  2435. } else {
  2436. //yl += this.padding.bottom;
  2437. yl += this.ibottom;
  2438. }
  2439. }
  2440. }
  2441. return { xi: xi, xl: xl, yi: yi, yl: yl };
  2442. };
  2443. Element.prototype._getShrinkContent = function(xi, xl, yi, yl, get) {
  2444. var h = this._clines.length
  2445. , w = this._clines.mwidth || 1;
  2446. if (this.position.width == null
  2447. && (this.position.left == null
  2448. || this.position.right == null)) {
  2449. if (this.position.left == null && this.position.right != null) {
  2450. xi = xl - w - this.iwidth;
  2451. } else {
  2452. xl = xi + w + this.iwidth;
  2453. }
  2454. }
  2455. if (this.position.height == null
  2456. && (this.position.top == null
  2457. || this.position.bottom == null)
  2458. && !this.scrollable) {
  2459. if (this.position.top == null && this.position.bottom != null) {
  2460. yi = yl - h - this.iheight;
  2461. } else {
  2462. yl = yi + h + this.iheight;
  2463. }
  2464. }
  2465. return { xi: xi, xl: xl, yi: yi, yl: yl };
  2466. };
  2467. Element.prototype._getShrink = function(xi, xl, yi, yl, get) {
  2468. var shrinkBox = this._getShrinkBox(xi, xl, yi, yl, get)
  2469. , shrinkContent = this._getShrinkContent(xi, xl, yi, yl, get)
  2470. , xll = xl
  2471. , yll = yl;
  2472. // Figure out which one is bigger and use it.
  2473. if (shrinkBox.xl - shrinkBox.xi > shrinkContent.xl - shrinkContent.xi) {
  2474. xi = shrinkBox.xi;
  2475. xl = shrinkBox.xl;
  2476. } else {
  2477. xi = shrinkContent.xi;
  2478. xl = shrinkContent.xl;
  2479. }
  2480. if (shrinkBox.yl - shrinkBox.yi > shrinkContent.yl - shrinkContent.yi) {
  2481. yi = shrinkBox.yi;
  2482. yl = shrinkBox.yl;
  2483. } else {
  2484. yi = shrinkContent.yi;
  2485. yl = shrinkContent.yl;
  2486. }
  2487. // Recenter shrunken elements.
  2488. if (xl < xll && this.position.left === 'center') {
  2489. xll = (xll - xl) / 2 | 0;
  2490. xi += xll;
  2491. xl += xll;
  2492. }
  2493. if (yl < yll && this.position.top === 'center') {
  2494. yll = (yll - yl) / 2 | 0;
  2495. yi += yll;
  2496. yl += yll;
  2497. }
  2498. return { xi: xi, xl: xl, yi: yi, yl: yl };
  2499. };
  2500. Element.prototype._getCoords = function(get, noscroll) {
  2501. if (this.hidden) return;
  2502. // if (this.parent._rendering) get = true;
  2503. var xi = this._getLeft(get)
  2504. , xl = xi + this._getWidth(get)
  2505. , yi = this._getTop(get)
  2506. , yl = yi + this._getHeight(get)
  2507. , base = this.childBase || 0
  2508. , el = this
  2509. , fixed = this.fixed
  2510. , coords
  2511. , v
  2512. , noleft
  2513. , noright
  2514. , notop
  2515. , nobot
  2516. , ppos
  2517. , b;
  2518. // Attempt to shrink the element base on the
  2519. // size of the content and child elements.
  2520. if (this.shrink) {
  2521. coords = this._getShrink(xi, xl, yi, yl, get);
  2522. xi = coords.xi, xl = coords.xl;
  2523. yi = coords.yi, yl = coords.yl;
  2524. }
  2525. // Find a scrollable ancestor if we have one.
  2526. while (el = el.parent) {
  2527. if (el.scrollable) {
  2528. if (fixed) {
  2529. fixed = false;
  2530. continue;
  2531. }
  2532. break;
  2533. }
  2534. }
  2535. // Check to make sure we're visible and
  2536. // inside of the visible scroll area.
  2537. // NOTE: Lists have a property where only
  2538. // the list items are obfuscated.
  2539. // Old way of doing things, this would not render right if a shrunken element
  2540. // with lots of boxes in it was within a scrollable element.
  2541. // See: $ node test/widget-shrink-fail.js
  2542. // var thisparent = this.parent;
  2543. var thisparent = el;
  2544. if (el && !noscroll) {
  2545. ppos = thisparent.lpos;
  2546. // The shrink option can cause a stack overflow
  2547. // by calling _getCoords on the child again.
  2548. // if (!get && !thisparent.shrink) {
  2549. // ppos = thisparent._getCoords();
  2550. // }
  2551. if (!ppos) return;
  2552. // TODO: Figure out how to fix base (and cbase to only
  2553. // take into account the *parent's* padding.
  2554. yi -= ppos.base;
  2555. yl -= ppos.base;
  2556. b = thisparent.border ? 1 : 0;
  2557. if (yi < ppos.yi + b) {
  2558. if (yl - 1 < ppos.yi + b) {
  2559. // Is above.
  2560. return;
  2561. } else {
  2562. // Is partially covered above.
  2563. notop = true;
  2564. v = ppos.yi - yi;
  2565. if (this.border) v--;
  2566. if (thisparent.border) v++;
  2567. base += v;
  2568. yi += v;
  2569. }
  2570. } else if (yl > ppos.yl - b) {
  2571. if (yi > ppos.yl - 1 - b) {
  2572. // Is below.
  2573. return;
  2574. } else {
  2575. // Is partially covered below.
  2576. nobot = true;
  2577. v = yl - ppos.yl;
  2578. if (this.border) v--;
  2579. if (thisparent.border) v++;
  2580. yl -= v;
  2581. }
  2582. }
  2583. // Shouldn't be necessary.
  2584. // assert.ok(yi < yl);
  2585. if (yi >= yl) return;
  2586. // Could allow overlapping stuff in scrolling elements
  2587. // if we cleared the pending buffer before every draw.
  2588. if (xi < el.lpos.xi) {
  2589. xi = el.lpos.xi;
  2590. noleft = true;
  2591. if (this.border) xi--;
  2592. if (thisparent.border) xi++;
  2593. }
  2594. if (xl > el.lpos.xl) {
  2595. xl = el.lpos.xl;
  2596. noright = true;
  2597. if (this.border) xl++;
  2598. if (thisparent.border) xl--;
  2599. }
  2600. //if (xi > xl) return;
  2601. if (xi >= xl) return;
  2602. }
  2603. if (this.noOverflow && this.parent.lpos) {
  2604. if (xi < this.parent.lpos.xi + this.parent.ileft) {
  2605. xi = this.parent.lpos.xi + this.parent.ileft;
  2606. }
  2607. if (xl > this.parent.lpos.xl - this.parent.iright) {
  2608. xl = this.parent.lpos.xl - this.parent.iright;
  2609. }
  2610. if (yi < this.parent.lpos.yi + this.parent.itop) {
  2611. yi = this.parent.lpos.yi + this.parent.itop;
  2612. }
  2613. if (yl > this.parent.lpos.yl - this.parent.ibottom) {
  2614. yl = this.parent.lpos.yl - this.parent.ibottom;
  2615. }
  2616. }
  2617. //if (this.parent.lpos) {
  2618. // this.parent.lpos._scrollBottom = Math.max(
  2619. // this.parent.lpos._scrollBottom, yl);
  2620. //}
  2621. return {
  2622. xi: xi,
  2623. xl: xl,
  2624. yi: yi,
  2625. yl: yl,
  2626. base: base,
  2627. noleft: noleft,
  2628. noright: noright,
  2629. notop: notop,
  2630. nobot: nobot,
  2631. renders: this.screen.renders
  2632. };
  2633. };
  2634. Element.prototype.render = function() {
  2635. this._emit('prerender');
  2636. this.parseContent();
  2637. var coords = this._getCoords(true);
  2638. if (!coords) {
  2639. delete this.lpos;
  2640. return;
  2641. }
  2642. var lines = this.screen.lines
  2643. , xi = coords.xi
  2644. , xl = coords.xl
  2645. , yi = coords.yi
  2646. , yl = coords.yl
  2647. , x
  2648. , y
  2649. , cell
  2650. , attr
  2651. , ch
  2652. , content = this._pcontent
  2653. , ci = this._clines.ci[coords.base]
  2654. , battr
  2655. , dattr
  2656. , c
  2657. , rtop
  2658. , visible
  2659. , i
  2660. , bch = this.ch;
  2661. if (coords.base >= this._clines.ci.length) {
  2662. ci = this._pcontent.length;
  2663. }
  2664. this.lpos = coords;
  2665. dattr = this.sattr(this.style, this.style.fg, this.style.bg);
  2666. attr = dattr;
  2667. // If we're in a scrollable text box, check to
  2668. // see which attributes this line starts with.
  2669. if (ci > 0) {
  2670. attr = this._clines.attr[Math.min(coords.base, this._clines.length - 1)];
  2671. }
  2672. if (this.border) xi++, xl--, yi++, yl--;
  2673. // If we have padding/valign, that means the
  2674. // content-drawing loop will skip a few cells/lines.
  2675. // To deal with this, we can just fill the whole thing
  2676. // ahead of time. This could be optimized.
  2677. if (this.tpadding || (this.valign && this.valign !== 'top')) {
  2678. this.screen.fillRegion(dattr, bch, xi, xl, yi, yl);
  2679. }
  2680. if (this.tpadding) {
  2681. xi += this.padding.left, xl -= this.padding.right;
  2682. yi += this.padding.top, yl -= this.padding.bottom;
  2683. }
  2684. // Determine where to place the text if it's vertically aligned.
  2685. if (this.valign === 'middle' || this.valign === 'bottom') {
  2686. visible = yl - yi;
  2687. if (this._clines.length < visible) {
  2688. if (this.valign === 'middle') {
  2689. visible = visible / 2 | 0;
  2690. visible -= this._clines.length / 2 | 0;
  2691. } else if (this.valign === 'bottom') {
  2692. visible -= this._clines.length;
  2693. }
  2694. yi += visible;
  2695. }
  2696. }
  2697. // Draw the content and background.
  2698. for (y = yi; y < yl; y++) {
  2699. if (!lines[y]) break;
  2700. for (x = xi; x < xl; x++) {
  2701. cell = lines[y][x];
  2702. if (!cell) break;
  2703. ch = content[ci++] || bch;
  2704. // if (!content[ci] && !coords._contentEnd) {
  2705. // coords._contentEnd = { x: x - xi, y: y - yi };
  2706. // }
  2707. // Handle escape codes.
  2708. while (ch === '\x1b') {
  2709. if (c = /^\x1b\[[\d;]*m/.exec(content.substring(ci - 1))) {
  2710. ci += c[0].length - 1;
  2711. attr = this.screen.attrCode(c[0], attr, dattr);
  2712. // Ignore foreground changes for selected items.
  2713. if (this.parent._isList
  2714. && this.parent.items[this.parent.selected] === this) {
  2715. attr = (attr & ~(0x1ff << 9)) | (dattr & (0x1ff << 9));
  2716. }
  2717. ch = content[ci] || bch;
  2718. ci++;
  2719. } else {
  2720. break;
  2721. }
  2722. }
  2723. // Handle newlines.
  2724. if (ch === '\t') ch = bch;
  2725. if (ch === '\n') {
  2726. // If we're on the first cell and we find a newline and the last cell
  2727. // of the last line was not a newline, let's just treat this like the
  2728. // newline was already "counted".
  2729. if (x === xi && y !== yi && content[ci-2] !== '\n') {
  2730. x--;
  2731. continue;
  2732. }
  2733. // We could use fillRegion here, name the
  2734. // outer loop, and continue to it instead.
  2735. ch = bch;
  2736. for (; x < xl; x++) {
  2737. cell = lines[y][x];
  2738. if (!cell) break;
  2739. if (attr !== cell[0] || ch !== cell[1]) {
  2740. lines[y][x][0] = attr;
  2741. lines[y][x][1] = ch;
  2742. lines[y].dirty = true;
  2743. }
  2744. }
  2745. continue;
  2746. }
  2747. if (attr !== cell[0] || ch !== cell[1]) {
  2748. lines[y][x][0] = attr;
  2749. lines[y][x][1] = ch;
  2750. lines[y].dirty = true;
  2751. }
  2752. // if (wideChars.test(ch)) {
  2753. // x++;
  2754. // lines[y][x][0] = attr;
  2755. // lines[y][x][1] = bch;
  2756. // }
  2757. }
  2758. }
  2759. // Draw the scrollbar.
  2760. // Could possibly draw this after all child elements.
  2761. if (this.scrollbar) {
  2762. i = Math.max(this._clines.length, this._scrollBottom());
  2763. }
  2764. if (coords.notop || coords.nobot) i = -Infinity;
  2765. if (this.scrollbar && (yl - yi) < i) {
  2766. x = xl - 1;
  2767. if (this.scrollbar.ignoreBorder && this.border) x++;
  2768. if (this.alwaysScroll) {
  2769. y = this.childBase / (i - (yl - yi));
  2770. } else {
  2771. y = (this.childBase + this.childOffset) / (i - 1);
  2772. }
  2773. y = yi + ((yl - yi) * y | 0);
  2774. if (y >= yl) y = yl - 1;
  2775. cell = lines[y] && lines[y][x];
  2776. if (cell) {
  2777. if (this.track) {
  2778. ch = this.track.ch || ' ';
  2779. attr = this.sattr(this.style.track,
  2780. this.style.track.fg || this.style.fg,
  2781. this.style.track.bg || this.style.bg);
  2782. this.screen.fillRegion(attr, ch, x, x + 1, yi, yl);
  2783. }
  2784. ch = this.scrollbar.ch || ' ';
  2785. attr = this.sattr(this.style.scrollbar,
  2786. this.style.scrollbar.fg || this.style.fg,
  2787. this.style.scrollbar.bg || this.style.bg);
  2788. if (attr !== cell[0] || ch !== cell[1]) {
  2789. lines[y][x][0] = attr;
  2790. lines[y][x][1] = ch;
  2791. lines[y].dirty = true;
  2792. }
  2793. }
  2794. }
  2795. if (this.border) xi--, xl++, yi--, yl++;
  2796. if (this.tpadding) {
  2797. xi -= this.padding.left, xl += this.padding.right;
  2798. yi -= this.padding.top, yl += this.padding.bottom;
  2799. }
  2800. // Draw the border.
  2801. if (this.border) {
  2802. battr = this.sattr(this.style.border,
  2803. this.style.border.fg, this.style.border.bg);
  2804. y = yi;
  2805. if (coords.notop) y = -1;
  2806. for (x = xi; x < xl; x++) {
  2807. if (!lines[y]) break;
  2808. if (coords.noleft && x === xi) continue;
  2809. if (coords.noright && x === xl - 1) continue;
  2810. if (this.border.type === 'line') {
  2811. if (x === xi) ch = '┌';
  2812. else if (x === xl - 1) ch = '┐';
  2813. else ch = '─';
  2814. } else if (this.border.type === 'bg') {
  2815. ch = this.border.ch;
  2816. }
  2817. cell = lines[y][x];
  2818. if (!cell) break;
  2819. if (battr !== cell[0] || ch !== cell[1]) {
  2820. lines[y][x][0] = battr;
  2821. lines[y][x][1] = ch;
  2822. lines[y].dirty = true;
  2823. }
  2824. }
  2825. y = yi + 1;
  2826. for (; y < yl - 1; y++) {
  2827. if (!lines[y]) break;
  2828. if (this.border.type === 'line') {
  2829. ch = '│';
  2830. } else if (this.border.type === 'bg') {
  2831. ch = this.border.ch;
  2832. }
  2833. cell = lines[y][xi];
  2834. if (!cell) break;
  2835. if (!coords.noleft)
  2836. if (battr !== cell[0] || ch !== cell[1]) {
  2837. lines[y][xi][0] = battr;
  2838. lines[y][xi][1] = ch;
  2839. lines[y].dirty = true;
  2840. }
  2841. cell = lines[y][xl - 1];
  2842. if (!cell) break;
  2843. if (!coords.noright)
  2844. if (battr !== cell[0] || ch !== cell[1]) {
  2845. lines[y][xl - 1][0] = battr;
  2846. lines[y][xl - 1][1] = ch;
  2847. lines[y].dirty = true;
  2848. }
  2849. }
  2850. y = yl - 1;
  2851. if (coords.nobot) y = -1;
  2852. for (x = xi; x < xl; x++) {
  2853. if (!lines[y]) break;
  2854. if (coords.noleft && x === xi) continue;
  2855. if (coords.noright && x === xl - 1) continue;
  2856. if (this.border.type === 'line') {
  2857. if (x === xi) ch = '└';
  2858. else if (x === xl - 1) ch = '┘';
  2859. else ch = '─';
  2860. } else if (this.border.type === 'bg') {
  2861. ch = this.border.ch;
  2862. }
  2863. cell = lines[y][x];
  2864. if (!cell) break;
  2865. if (battr !== cell[0] || ch !== cell[1]) {
  2866. lines[y][x][0] = battr;
  2867. lines[y][x][1] = ch;
  2868. lines[y].dirty = true;
  2869. }
  2870. }
  2871. }
  2872. this.children.forEach(function(el) {
  2873. if (el.screen._ci !== -1) {
  2874. el.index = el.screen._ci++;
  2875. }
  2876. //if (el.screen._rendering) el._rendering = true;
  2877. el.render();
  2878. //if (el.screen._rendering) el._rendering = false;
  2879. });
  2880. this._emit('render', [coords]);
  2881. return coords;
  2882. };
  2883. Element.prototype._render = Element.prototype.render;
  2884. Element.prototype.insertLine = function(i, line) {
  2885. if (typeof line === 'string') line = line.split('\n');
  2886. if (i !== i || i == null) {
  2887. i = this._clines.ftor.length;
  2888. }
  2889. i = Math.max(i, 0);
  2890. while (this._clines.fake.length < i) {
  2891. this._clines.fake.push('');
  2892. this._clines.ftor.push([this._clines.push('') - 1]);
  2893. this._clines.rtof(this._clines.fake.length - 1);
  2894. }
  2895. // NOTE: Could possibly compare the first and last ftor line numbers to see
  2896. // if they're the same, or if they fit in the visible region entirely.
  2897. var start = this._clines.length
  2898. , diff
  2899. , real;
  2900. if (i >= this._clines.ftor.length) {
  2901. real = this._clines.ftor[this._clines.ftor.length - 1];
  2902. real = real[real.length-1] + 1;
  2903. } else {
  2904. real = this._clines.ftor[i][0];
  2905. }
  2906. for (var j = 0; j < line.length; j++) {
  2907. this._clines.fake.splice(i + j, 0, line[j]);
  2908. }
  2909. this.setContent(this._clines.fake.join('\n'), true);
  2910. diff = this._clines.length - start;
  2911. if (diff > 0) {
  2912. var pos = this._getCoords();
  2913. if (!pos) return;
  2914. var height = pos.yl - pos.yi - this.iheight
  2915. , base = this.childBase || 0
  2916. , visible = real >= base && real - base < height;
  2917. if (pos && visible && this.screen.cleanSides(this)) {
  2918. this.screen.insertLine(diff,
  2919. pos.yi + this.itop + real - base,
  2920. pos.yi,
  2921. pos.yl - this.ibottom - 1);
  2922. }
  2923. }
  2924. };
  2925. Element.prototype.deleteLine = function(i, n) {
  2926. n = n || 1;
  2927. if (i !== i || i == null) {
  2928. i = this._clines.ftor.length - 1;
  2929. }
  2930. i = Math.max(i, 0);
  2931. i = Math.min(i, this._clines.ftor.length - 1);
  2932. // NOTE: Could possibly compare the first and last ftor line numbers to see
  2933. // if they're the same, or if they fit in the visible region entirely.
  2934. var start = this._clines.length
  2935. , diff
  2936. , real = this._clines.ftor[i][0];
  2937. while (n--) {
  2938. this._clines.fake.splice(i, 1);
  2939. }
  2940. this.setContent(this._clines.fake.join('\n'), true);
  2941. diff = start - this._clines.length;
  2942. if (diff > 0) {
  2943. var pos = this._getCoords();
  2944. if (!pos) return;
  2945. var height = pos.yl - pos.yi - this.iheight
  2946. , base = this.childBase || 0
  2947. , visible = real >= base && real - base < height;
  2948. if (pos && visible && this.screen.cleanSides(this)) {
  2949. this.screen.deleteLine(diff,
  2950. pos.yi + this.itop + real - base,
  2951. pos.yi,
  2952. pos.yl - this.ibottom - 1);
  2953. }
  2954. }
  2955. if (this._clines.length < height) {
  2956. this.clearPos();
  2957. }
  2958. };
  2959. Element.prototype.insertTop = function(line) {
  2960. var fake = this._clines.rtof[this.childBase || 0];
  2961. return this.insertLine(fake, line);
  2962. };
  2963. Element.prototype.insertBottom = function(line) {
  2964. var h = (this.childBase || 0) + this.height - this.iheight
  2965. , i = Math.min(h, this._clines.length)
  2966. , fake = this._clines.rtof[i - 1] + 1;
  2967. return this.insertLine(fake, line);
  2968. };
  2969. Element.prototype.deleteTop = function(n) {
  2970. var fake = this._clines.rtof[this.childBase || 0];
  2971. return this.deleteLine(fake, n);
  2972. };
  2973. Element.prototype.deleteBottom = function(n) {
  2974. var h = (this.childBase || 0) + this.height - 1 - this.iheight
  2975. , i = Math.min(h, this._clines.length - 1)
  2976. , n = n || 1
  2977. , fake = this._clines.rtof[i];
  2978. return this.deleteLine(fake - (n - 1), n);
  2979. };
  2980. Element.prototype.setLine = function(i, line) {
  2981. i = Math.max(i, 0);
  2982. while (this._clines.fake.length < i) {
  2983. this._clines.fake.push('');
  2984. }
  2985. this._clines.fake[i] = line;
  2986. return this.setContent(this._clines.fake.join('\n'), true);
  2987. };
  2988. Element.prototype.setBaseLine = function(i, line) {
  2989. var fake = this._clines.rtof[this.childBase || 0];
  2990. return this.setLine(fake + i, line);
  2991. };
  2992. Element.prototype.getLine = function(i) {
  2993. i = Math.max(i, 0);
  2994. i = Math.min(i, this._clines.fake.length - 1);
  2995. return this._clines.fake[i];
  2996. };
  2997. Element.prototype.getBaseLine = function(i) {
  2998. var fake = this._clines.rtof[this.childBase || 0];
  2999. return this.getLine(fake + i);
  3000. };
  3001. Element.prototype.clearLine = function(i) {
  3002. i = Math.min(i, this._clines.fake.length - 1);
  3003. return this.setLine(i, '');
  3004. };
  3005. Element.prototype.clearBaseLine = function(i) {
  3006. var fake = this._clines.rtof[this.childBase || 0];
  3007. return this.clearLine(fake + i);
  3008. };
  3009. Element.prototype.unshiftLine = function(line) {
  3010. return this.insertLine(0, line);
  3011. };
  3012. Element.prototype.shiftLine = function(n) {
  3013. return this.deleteLine(0, n);
  3014. };
  3015. Element.prototype.pushLine = function(line) {
  3016. return this.insertLine(this._clines.fake.length, line);
  3017. };
  3018. Element.prototype.popLine = function(n) {
  3019. return this.deleteLine(this._clines.fake.length - 1, n);
  3020. };
  3021. Element.prototype.getLines = function() {
  3022. return this._clines.fake.slice();
  3023. };
  3024. Element.prototype.getScreenLines = function() {
  3025. return this._clines.slice();
  3026. };
  3027. /**
  3028. * Box
  3029. */
  3030. function Box(options) {
  3031. if (!(this instanceof Node)) {
  3032. return new Box(options);
  3033. }
  3034. options = options || {};
  3035. Element.call(this, options);
  3036. }
  3037. Box.prototype.__proto__ = Element.prototype;
  3038. Box.prototype.type = 'box';
  3039. /**
  3040. * Text
  3041. */
  3042. function Text(options) {
  3043. if (!(this instanceof Node)) {
  3044. return new Text(options);
  3045. }
  3046. options = options || {};
  3047. options.shrink = true;
  3048. Element.call(this, options);
  3049. }
  3050. Text.prototype.__proto__ = Element.prototype;
  3051. Text.prototype.type = 'text';
  3052. /**
  3053. * Line
  3054. */
  3055. function Line(options) {
  3056. var self = this;
  3057. if (!(this instanceof Node)) {
  3058. return new Line(options);
  3059. }
  3060. options = options || {};
  3061. var orientation = options.orientation || 'vertical';
  3062. delete options.orientation;
  3063. if (orientation === 'vertical') {
  3064. options.width = 1;
  3065. } else {
  3066. options.height = 1;
  3067. }
  3068. Box.call(this, options);
  3069. this.ch = !options.type || options.type === 'line'
  3070. ? orientation === 'horizontal' ? '─' : '│'
  3071. : options.ch || ' ';
  3072. this.border = {
  3073. type: 'bg',
  3074. __proto__: this
  3075. // get ch() { return self.ch; },
  3076. // set ch(c) { return self.ch = c; }
  3077. };
  3078. this.style.border = this.style;
  3079. // Maybe instead of the above:
  3080. // this.on('prerender', function() {
  3081. // self._style = self.style;
  3082. // self.border = { type: 'bg', ch: self.ch };
  3083. // self.style = { border: self.style };
  3084. // });
  3085. //
  3086. // this.on('render', function(coords) {
  3087. // self.style = self._style;
  3088. // });
  3089. }
  3090. Line.prototype.__proto__ = Box.prototype;
  3091. Line.prototype.type = 'line';
  3092. /**
  3093. * ScrollableBox
  3094. */
  3095. function ScrollableBox(options) {
  3096. var self = this;
  3097. if (!(this instanceof Node)) {
  3098. return new ScrollableBox(options);
  3099. }
  3100. options = options || {};
  3101. Box.call(this, options);
  3102. if (options.scrollable === false) {
  3103. return this;
  3104. }
  3105. this.scrollable = true;
  3106. this.childOffset = 0;
  3107. this.childBase = 0;
  3108. this.baseLimit = options.baseLimit || Infinity;
  3109. this.alwaysScroll = options.alwaysScroll;
  3110. this.scrollbar = options.scrollbar;
  3111. if (this.scrollbar) {
  3112. this.scrollbar.ch = this.scrollbar.ch || ' ';
  3113. this.style.scrollbar = this.style.scrollbar || this.scrollbar.style;
  3114. if (!this.style.scrollbar) {
  3115. this.style.scrollbar = {};
  3116. this.style.scrollbar.fg = this.scrollbar.fg;
  3117. this.style.scrollbar.bg = this.scrollbar.bg;
  3118. this.style.scrollbar.bold = this.scrollbar.bold;
  3119. this.style.scrollbar.underline = this.scrollbar.underline;
  3120. this.style.scrollbar.inverse = this.scrollbar.inverse;
  3121. this.style.scrollbar.invisible = this.scrollbar.invisible;
  3122. }
  3123. this.scrollbar.style = this.style.scrollbar;
  3124. if (this.track || this.scrollbar.track) {
  3125. this.track = this.scrollbar.track || this.track;
  3126. this.style.track = this.style.scrollbar.track || this.style.track;
  3127. this.track.ch = this.track.ch || ' ';
  3128. this.style.track = this.style.track || this.track.style;
  3129. if (!this.style.track) {
  3130. this.style.track = {};
  3131. this.style.track.fg = this.track.fg;
  3132. this.style.track.bg = this.track.bg;
  3133. this.style.track.bold = this.track.bold;
  3134. this.style.track.underline = this.track.underline;
  3135. this.style.track.inverse = this.track.inverse;
  3136. this.style.track.invisible = this.track.invisible;
  3137. }
  3138. this.track.style = this.style.track;
  3139. }
  3140. }
  3141. if (options.mouse) {
  3142. this.on('wheeldown', function(el, data) {
  3143. self.scroll(self.height / 2 | 0 || 1);
  3144. self.screen.render();
  3145. });
  3146. this.on('wheelup', function(el, data) {
  3147. self.scroll(-(self.height / 2 | 0) || -1);
  3148. self.screen.render();
  3149. });
  3150. }
  3151. if (options.keys && !options.ignoreKeys) {
  3152. this.on('keypress', function(ch, key) {
  3153. if (key.name === 'up' || (options.vi && key.name === 'k')) {
  3154. self.scroll(-1);
  3155. self.screen.render();
  3156. return;
  3157. }
  3158. if (key.name === 'down' || (options.vi && key.name === 'j')) {
  3159. self.scroll(1);
  3160. self.screen.render();
  3161. return;
  3162. }
  3163. if (options.vi && key.name === 'u' && key.ctrl) {
  3164. self.scroll(-(self.height / 2 | 0) || -1);
  3165. self.screen.render();
  3166. return;
  3167. }
  3168. if (options.vi && key.name === 'd' && key.ctrl) {
  3169. self.scroll(self.height / 2 | 0 || 1);
  3170. self.screen.render();
  3171. return;
  3172. }
  3173. if (options.vi && key.name === 'b' && key.ctrl) {
  3174. self.scroll(-self.height || -1);
  3175. self.screen.render();
  3176. return;
  3177. }
  3178. if (options.vi && key.name === 'f' && key.ctrl) {
  3179. self.scroll(self.height || 1);
  3180. self.screen.render();
  3181. return;
  3182. }
  3183. if (options.vi && key.name === 'g' && !key.shift) {
  3184. self.scroll(-self._clines.length);
  3185. self.screen.render();
  3186. return;
  3187. }
  3188. if (options.vi && key.name === 'g' && key.shift) {
  3189. self.scroll(self._clines.length);
  3190. self.screen.render();
  3191. return;
  3192. }
  3193. });
  3194. }
  3195. this.on('parsed content', function() {
  3196. self._recalculateIndex();
  3197. });
  3198. self._recalculateIndex();
  3199. }
  3200. ScrollableBox.prototype.__proto__ = Box.prototype;
  3201. ScrollableBox.prototype.type = 'scrollable-box';
  3202. ScrollableBox.prototype._scrollBottom = function() {
  3203. if (!this.scrollable) return 0;
  3204. // We could just calculate the children, but we can
  3205. // optimize for lists by just returning the items.length.
  3206. if (this._isList) {
  3207. return this.items ? this.items.length : 0;
  3208. }
  3209. if (this.lpos && this.lpos._scrollBottom) {
  3210. return this.lpos._scrollBottom;
  3211. }
  3212. var bottom = this.children.reduce(function(current, el) {
  3213. // el.height alone does not calculate the shrunken height, we need to use
  3214. // getCoords. A shrunken box inside a scrollable element will not grow any
  3215. // larger than the scrollable element's context regardless of how much
  3216. // content is in the shrunken box, unless we do this (call getCoords
  3217. // without the scrollable calculation):
  3218. // See: $ node test/widget-shrink-fail-2.js
  3219. if (!el.detached) {
  3220. var lpos = el._getCoords(false, true);
  3221. if (lpos) {
  3222. return Math.max(current, el.rtop + (lpos.yl - lpos.yi));
  3223. }
  3224. }
  3225. return Math.max(current, el.rtop + el.height);
  3226. }, 0);
  3227. if (this.lpos) this.lpos._scrollBottom = bottom;
  3228. return bottom;
  3229. };
  3230. ScrollableBox.prototype.setScroll =
  3231. ScrollableBox.prototype.scrollTo = function(offset) {
  3232. return this.scroll(offset - (this.childBase + this.childOffset));
  3233. };
  3234. ScrollableBox.prototype.getScroll = function() {
  3235. return this.childBase + this.childOffset;
  3236. };
  3237. ScrollableBox.prototype.scroll = function(offset, always) {
  3238. if (!this.scrollable) return;
  3239. if (this.detached) return;
  3240. // Handle scrolling.
  3241. var visible = this.height - this.iheight
  3242. , base = this.childBase
  3243. , d
  3244. , p
  3245. , t
  3246. , b
  3247. , max
  3248. , emax;
  3249. if (this.alwaysScroll || always) {
  3250. // Semi-workaround
  3251. this.childOffset = offset > 0
  3252. ? visible - 1 + offset
  3253. : offset;
  3254. } else {
  3255. this.childOffset += offset;
  3256. }
  3257. if (this.childOffset > visible - 1) {
  3258. d = this.childOffset - (visible - 1);
  3259. this.childOffset -= d;
  3260. this.childBase += d;
  3261. } else if (this.childOffset < 0) {
  3262. d = this.childOffset;
  3263. this.childOffset += -d;
  3264. this.childBase += d;
  3265. }
  3266. if (this.childBase < 0) {
  3267. this.childBase = 0;
  3268. } else if (this.childBase > this.baseLimit) {
  3269. this.childBase = this.baseLimit;
  3270. }
  3271. // Find max "bottom" value for
  3272. // content and descendant elements.
  3273. // Scroll the content if necessary.
  3274. if (this.childBase === base) {
  3275. return this.emit('scroll');
  3276. }
  3277. // When scrolling text, we want to be able to handle SGR codes as well as line
  3278. // feeds. This allows us to take preformatted text output from other programs
  3279. // and put it in a scrollable text box.
  3280. this.parseContent();
  3281. max = this._clines.length - (this.height - this.iheight);
  3282. if (max < 0) max = 0;
  3283. emax = this._scrollBottom() - (this.height - this.iheight);
  3284. if (emax < 0) emax = 0;
  3285. this.childBase = Math.min(this.childBase, Math.max(emax, max));
  3286. if (this.childBase < 0) {
  3287. this.childBase = 0;
  3288. } else if (this.childBase > this.baseLimit) {
  3289. this.childBase = this.baseLimit;
  3290. }
  3291. // Optimize scrolling with CSR + IL/DL.
  3292. p = this.lpos;
  3293. // Only really need _getCoords() if we want
  3294. // to allow nestable scrolling elements...
  3295. // or if we **really** want shrinkable
  3296. // scrolling elements.
  3297. // p = this._getCoords();
  3298. if (p && this.childBase !== base && this.screen.cleanSides(this)) {
  3299. t = p.yi + this.itop;
  3300. b = p.yl - this.ibottom - 1;
  3301. d = this.childBase - base;
  3302. if (d > 0 && d < visible) {
  3303. // scrolled down
  3304. this.screen.deleteLine(d, t, t, b);
  3305. } else if (d < 0 && -d < visible) {
  3306. // scrolled up
  3307. d = -d;
  3308. this.screen.insertLine(d, t, t, b);
  3309. }
  3310. }
  3311. return this.emit('scroll');
  3312. };
  3313. ScrollableBox.prototype._recalculateIndex = function() {
  3314. var max, emax;
  3315. if (this.detached || !this.scrollable) {
  3316. return 0;
  3317. }
  3318. max = this._clines.length - (this.height - this.iheight);
  3319. if (max < 0) max = 0;
  3320. emax = this._scrollBottom() - (this.height - this.iheight);
  3321. if (emax < 0) emax = 0;
  3322. this.childBase = Math.min(this.childBase, Math.max(emax, max));
  3323. if (this.childBase < 0) {
  3324. this.childBase = 0;
  3325. } else if (this.childBase > this.baseLimit) {
  3326. this.childBase = this.baseLimit;
  3327. }
  3328. };
  3329. ScrollableBox.prototype.resetScroll = function() {
  3330. if (!this.scrollable) return;
  3331. this.childOffset = 0;
  3332. this.childBase = 0;
  3333. return this.emit('scroll');
  3334. };
  3335. ScrollableBox.prototype.getScrollHeight = function() {
  3336. return Math.max(this._clines.length, this._scrollBottom());
  3337. };
  3338. ScrollableBox.prototype.getScrollPerc = function(s) {
  3339. var pos = this.lpos || this._getCoords();
  3340. if (!pos) return s ? -1 : 0;
  3341. var height = (pos.yl - pos.yi) - this.iheight
  3342. , i = this.getScrollHeight()
  3343. , p;
  3344. if (height < i) {
  3345. if (this.alwaysScroll) {
  3346. p = this.childBase / (i - height);
  3347. } else {
  3348. p = (this.childBase + this.childOffset) / (i - 1);
  3349. }
  3350. return p * 100;
  3351. }
  3352. return s ? -1 : 0;
  3353. };
  3354. ScrollableBox.prototype.setScrollPerc = function(i) {
  3355. var m = Math.max(this._clines.length, this._scrollBottom());
  3356. return this.scrollTo((i / 100) * m | 0);
  3357. };
  3358. /**
  3359. * ScrollableText
  3360. */
  3361. function ScrollableText(options) {
  3362. if (!(this instanceof Node)) {
  3363. return new ScrollableText(options);
  3364. }
  3365. options = options || {};
  3366. options.scrollable = true;
  3367. options.alwaysScroll = true;
  3368. Box.call(this, options);
  3369. }
  3370. ScrollableText.prototype.__proto__ = Box.prototype;
  3371. ScrollableText.prototype.type = 'scrollable-text';
  3372. /**
  3373. * List
  3374. */
  3375. function List(options) {
  3376. var self = this;
  3377. if (!(this instanceof Node)) {
  3378. return new List(options);
  3379. }
  3380. options = options || {};
  3381. options.ignoreKeys = true;
  3382. // Possibly put this here: this.items = [];
  3383. options.scrollable = true;
  3384. Box.call(this, options);
  3385. this.value = '';
  3386. this.items = [];
  3387. this.ritems = [];
  3388. this.selected = 0;
  3389. this._isList = true;
  3390. if (!this.style.selected) {
  3391. this.style.selected = {};
  3392. this.style.selected.bg = options.selectedBg;
  3393. this.style.selected.fg = options.selectedFg;
  3394. this.style.selected.bold = options.selectedBold;
  3395. this.style.selected.underline = options.selectedUnderline;
  3396. this.style.selected.blink = options.selectedBlink;
  3397. this.style.selected.inverse = options.selectedInverse;
  3398. this.style.selected.invisible = options.selectedInvisible;
  3399. }
  3400. if (!this.style.item) {
  3401. this.style.item = {};
  3402. this.style.item.bg = options.itemBg;
  3403. this.style.item.fg = options.itemFg;
  3404. this.style.item.bold = options.itemBold;
  3405. this.style.item.underline = options.itemUnderline;
  3406. this.style.item.blink = options.itemBlink;
  3407. this.style.item.inverse = options.itemInverse;
  3408. this.style.item.invisible = options.itemInvisible;
  3409. }
  3410. // Legacy: for apps written before the addition of item attributes.
  3411. ['bg', 'fg', 'bold', 'underline',
  3412. 'blink', 'inverse', 'invisible'].forEach(function(name) {
  3413. if (self.style[name] != null && self.style.item[name] == null) {
  3414. self.style.item[name] = self.style[name];
  3415. }
  3416. });
  3417. if (this.options.itemHoverBg) {
  3418. this.options.itemHoverEffects = { bg: this.options.itemHoverBg };
  3419. }
  3420. if (this.options.itemHoverEffects) {
  3421. this.style.item.hover = this.options.itemHoverEffects;
  3422. }
  3423. if (this.options.itemFocusEffects) {
  3424. this.style.item.focus = this.options.itemFocusEffects;
  3425. }
  3426. this.mouse = options.mouse || false;
  3427. if (options.items) {
  3428. this.ritems = options.items;
  3429. options.items.forEach(this.add.bind(this));
  3430. }
  3431. this.select(0);
  3432. if (options.mouse) {
  3433. this.screen._listenMouse(this);
  3434. //this.screen.on('element wheeldown', function(el, data) {
  3435. this.on('element wheeldown', function(el, data) {
  3436. // if (el !== self && !el.hasAncestor(self)) return;
  3437. self.select(self.selected + 2);
  3438. self.screen.render();
  3439. });
  3440. //this.screen.on('element wheelup', function(el, data) {
  3441. this.on('element wheelup', function(el, data) {
  3442. // if (el !== self && !el.hasAncestor(self)) return;
  3443. self.select(self.selected - 2);
  3444. self.screen.render();
  3445. });
  3446. }
  3447. if (options.keys) {
  3448. this.on('keypress', function(ch, key) {
  3449. if (key.name === 'up' || (options.vi && key.name === 'k')) {
  3450. self.up();
  3451. self.screen.render();
  3452. return;
  3453. }
  3454. if (key.name === 'down' || (options.vi && key.name === 'j')) {
  3455. self.down();
  3456. self.screen.render();
  3457. return;
  3458. }
  3459. if (key.name === 'enter'
  3460. || (options.vi && key.name === 'l' && !key.shift)) {
  3461. self.enterSelected();
  3462. return;
  3463. }
  3464. if (key.name === 'escape' || (options.vi && key.name === 'q')) {
  3465. self.cancelSelected();
  3466. return;
  3467. }
  3468. if (options.vi && key.name === 'u' && key.ctrl) {
  3469. self.move(-((self.height - self.iheight) / 2) | 0);
  3470. self.screen.render();
  3471. return;
  3472. }
  3473. if (options.vi && key.name === 'd' && key.ctrl) {
  3474. self.move((self.height - self.iheight) / 2 | 0);
  3475. self.screen.render();
  3476. return;
  3477. }
  3478. if (options.vi && key.name === 'b' && key.ctrl) {
  3479. self.move(-(self.height - self.iheight));
  3480. self.screen.render();
  3481. return;
  3482. }
  3483. if (options.vi && key.name === 'f' && key.ctrl) {
  3484. self.move(self.height - self.iheight);
  3485. self.screen.render();
  3486. return;
  3487. }
  3488. if (options.vi && key.name === 'h' && key.shift) {
  3489. self.move(self.childBase - self.selected);
  3490. self.screen.render();
  3491. return;
  3492. }
  3493. if (options.vi && key.name === 'm' && key.shift) {
  3494. // TODO: Maybe use Math.min(this.items.length,
  3495. // ... for calculating visible items elsewhere.
  3496. var visible = Math.min(
  3497. self.height - self.iheight,
  3498. self.items.length) / 2 | 0;
  3499. self.move(self.childBase + visible - self.selected);
  3500. self.screen.render();
  3501. return;
  3502. }
  3503. if (options.vi && key.name === 'l' && key.shift) {
  3504. // XXX This goes one too far on lists with an odd number of items.
  3505. self.down(self.childBase
  3506. + Math.min(self.height - self.iheight, self.items.length)
  3507. - self.selected);
  3508. self.screen.render();
  3509. return;
  3510. }
  3511. if (options.vi && key.name === 'g' && !key.shift) {
  3512. self.select(0);
  3513. self.screen.render();
  3514. return;
  3515. }
  3516. if (options.vi && key.name === 'g' && key.shift) {
  3517. self.select(self.items.length - 1);
  3518. self.screen.render();
  3519. return;
  3520. }
  3521. if (options.vi && key.ch === '/') {
  3522. if (typeof self.options.search !== 'function') {
  3523. return;
  3524. }
  3525. return self.options.search(function(searchString){
  3526. self.select(self.fuzzyFind(searchString));
  3527. self.screen.render();
  3528. })
  3529. }
  3530. });
  3531. }
  3532. this.on('resize', function() {
  3533. var visible = self.height - self.iheight;
  3534. if (visible >= self.selected + 1) {
  3535. //if (self.selected < visible - 1) {
  3536. self.childBase = 0;
  3537. self.childOffset = self.selected;
  3538. } else {
  3539. // Is this supposed to be: self.childBase = visible - self.selected + 1; ?
  3540. self.childBase = self.selected - visible + 1;
  3541. self.childOffset = visible - 1;
  3542. }
  3543. });
  3544. this.on('adopt', function(el) {
  3545. if (!~self.items.indexOf(el)) {
  3546. el.fixed = true;
  3547. }
  3548. });
  3549. // Ensure children are removed from the
  3550. // item list if they are items.
  3551. this.on('remove', function(el) {
  3552. self.removeItem(el);
  3553. });
  3554. }
  3555. List.prototype.__proto__ = Box.prototype;
  3556. List.prototype.type = 'list';
  3557. List.prototype.add =
  3558. List.prototype.addItem =
  3559. List.prototype.appendItem = function(item) {
  3560. var self = this;
  3561. this.ritems.push(item);
  3562. // Note: Could potentially use Button here.
  3563. var options = {
  3564. screen: this.screen,
  3565. content: item,
  3566. align: this.align || 'left',
  3567. top: this.itop + this.items.length,
  3568. left: this.ileft + 1,
  3569. right: this.iright + 1,
  3570. tags: this.parseTags,
  3571. height: 1,
  3572. hoverEffects: this.mouse ? this.style.item.hover : null,
  3573. focusEffects: this.mouse ? this.style.item.focus : null,
  3574. autoFocus: false
  3575. };
  3576. if (this.screen.autoPadding) {
  3577. options.top = this.items.length;
  3578. options.left = 1;
  3579. options.right = 1;
  3580. }
  3581. ['bg', 'fg', 'bold', 'underline',
  3582. 'blink', 'inverse', 'invisible'].forEach(function(name) {
  3583. options[name] = function() {
  3584. var attr = self.items[self.selected] === item
  3585. ? self.style.selected[name]
  3586. : self.style.item[name];
  3587. if (typeof attr === 'function') attr = attr(item);
  3588. return attr;
  3589. };
  3590. });
  3591. var item = new Box(options);
  3592. this.items.push(item);
  3593. this.append(item);
  3594. if (this.items.length === 1) {
  3595. this.select(0);
  3596. }
  3597. if (this.mouse) {
  3598. item.on('click', function(data) {
  3599. if (self.items[self.selected] === item) {
  3600. self.emit('action', item, self.selected);
  3601. self.emit('select', item, self.selected);
  3602. return;
  3603. }
  3604. self.select(item);
  3605. self.screen.render();
  3606. });
  3607. }
  3608. };
  3609. List.prototype.fuzzyFind = function(search) {
  3610. var index = this.getItemIndex(this.selected);
  3611. for (var i = 0; i < this.ritems.length; i++){
  3612. if (this.ritems[i].indexOf(search) === 0) {
  3613. return i;
  3614. }
  3615. }
  3616. return index;
  3617. };
  3618. List.prototype.getItemIndex = function(child) {
  3619. if (typeof child === 'number') {
  3620. return child;
  3621. } else if (typeof child === 'string') {
  3622. return this.ritems.indexOf(child);
  3623. } else {
  3624. return this.items.indexOf(child);
  3625. }
  3626. };
  3627. List.prototype.getItem = function(child) {
  3628. return this.items[this.getItemIndex(child)];
  3629. };
  3630. List.prototype.removeItem = function(child) {
  3631. var i = this.getItemIndex(child);
  3632. if (~i && this.items[i]) {
  3633. child = this.items.splice(i, 1)[0];
  3634. this.ritems.splice(i, 1);
  3635. this.remove(child);
  3636. if (i === this.selected) {
  3637. this.select(i - 1);
  3638. }
  3639. }
  3640. };
  3641. List.prototype.clearItems = function() {
  3642. return this.setItems([]);
  3643. };
  3644. List.prototype.setItems = function(items) {
  3645. var items = items.slice()
  3646. , original = this.items.slice()
  3647. , selected = this.selected
  3648. , sel = this.ritems[this.selected]
  3649. , i = 0;
  3650. this.select(0);
  3651. for (; i < items.length; i++) {
  3652. if (this.items[i]) {
  3653. this.items[i].setContent(items[i]);
  3654. } else {
  3655. this.add(items[i]);
  3656. }
  3657. }
  3658. for (; i < original.length; i++) {
  3659. this.remove(original[i]);
  3660. }
  3661. this.ritems = items;
  3662. // Try to find our old item if it still exists.
  3663. sel = items.indexOf(sel);
  3664. if (~sel) {
  3665. this.select(sel);
  3666. } else if (items.length === original.length) {
  3667. this.select(selected);
  3668. } else {
  3669. this.select(Math.min(selected, items.length - 1));
  3670. }
  3671. };
  3672. List.prototype.select = function(index) {
  3673. if (!this.items.length) {
  3674. this.selected = 0;
  3675. this.value = '';
  3676. this.scrollTo(0);
  3677. return;
  3678. }
  3679. if (typeof index === 'object') {
  3680. index = this.items.indexOf(index);
  3681. }
  3682. if (index < 0) index = 0;
  3683. else if (index >= this.items.length) index = this.items.length - 1;
  3684. if (this.selected === index && this._listInitialized) return;
  3685. this._listInitialized = true;
  3686. this.selected = index;
  3687. this.value = this.ritems[this.selected];
  3688. this.scrollTo(this.selected);
  3689. };
  3690. List.prototype.move = function(offset) {
  3691. this.select(this.selected + offset);
  3692. };
  3693. List.prototype.up = function(offset) {
  3694. this.move(-(offset || 1));
  3695. };
  3696. List.prototype.down = function(offset) {
  3697. this.move(offset || 1);
  3698. };
  3699. List.prototype.pick = function(label, callback) {
  3700. if (!callback) {
  3701. callback = label;
  3702. label = null;
  3703. }
  3704. var self = this;
  3705. var focused = this.screen.focused;
  3706. if (focused && focused._done) focused._done('stop');
  3707. this.screen.saveFocus();
  3708. //var parent = this.parent;
  3709. //this.detach();
  3710. //parent.append(this);
  3711. this.focus();
  3712. this.show();
  3713. this.select(0);
  3714. if (label) this.setLabel(label);
  3715. this.screen.render();
  3716. this.once('action', function(el, selected) {
  3717. if (label) self.removeLabel();
  3718. self.screen.restoreFocus();
  3719. self.hide();
  3720. self.screen.render();
  3721. if (!el) return callback();
  3722. return callback(null, self.ritems[selected]);
  3723. });
  3724. };
  3725. List.prototype.enterSelected = function(i) {
  3726. if (i != null) this.select(i);
  3727. this.emit('action', this.items[this.selected], this.selected);
  3728. this.emit('select', this.items[this.selected], this.selected);
  3729. };
  3730. List.prototype.cancelSelected = function(i) {
  3731. if (i != null) this.select(i);
  3732. //this.emit('action', this.items[this.selected], this.selected);
  3733. this.emit('action');
  3734. this.emit('cancel');
  3735. };
  3736. /**
  3737. * Form
  3738. */
  3739. function Form(options) {
  3740. var self = this;
  3741. if (!(this instanceof Node)) {
  3742. return new Form(options);
  3743. }
  3744. options = options || {};
  3745. options.ignoreKeys = true;
  3746. Box.call(this, options);
  3747. //this.on('element focus', function(el, ch, key) {
  3748. // self._refresh();
  3749. // if (~self._children.indexOf(el)) {
  3750. // self._selected = el;
  3751. // }
  3752. //});
  3753. if (options.keys) {
  3754. this.screen._listenKeys(this);
  3755. //this.screen.on('element keypress', function(el, ch, key) {
  3756. this.on('element keypress', function(el, ch, key) {
  3757. // Make sure we're not entering input into a textbox.
  3758. // if (self.screen.grabKeys || self.screen.lockKeys) {
  3759. // return;
  3760. // }
  3761. // Make sure we're a form or input element.
  3762. // if (el !== self && !el.hasAncestor(self)) return;
  3763. if ((key.name === 'tab' && !key.shift)
  3764. || (el.type === 'textbox' && options.autoNext && key.name === 'enter')
  3765. || key.name === 'down'
  3766. || (options.vi && key.name === 'j')) {
  3767. if (el.type === 'textbox' || el.type === 'textarea') {
  3768. if (key.name === 'j') return;
  3769. if (key.name === 'tab') {
  3770. // Workaround, since we can't stop the tab from being added.
  3771. el.emit('keypress', null, { name: 'backspace' });
  3772. }
  3773. el.emit('keypress', '\x1b', { name: 'escape' });
  3774. }
  3775. self.focusNext();
  3776. return;
  3777. }
  3778. if ((key.name === 'tab' && key.shift)
  3779. || key.name === 'up'
  3780. || (options.vi && key.name === 'k')) {
  3781. if (el.type === 'textbox' || el.type === 'textarea') {
  3782. if (key.name === 'k') return;
  3783. el.emit('keypress', '\x1b', { name: 'escape' });
  3784. }
  3785. self.focusPrevious();
  3786. return;
  3787. }
  3788. if (key.name === 'escape') {
  3789. self.focus();
  3790. return;
  3791. }
  3792. });
  3793. }
  3794. }
  3795. Form.prototype.__proto__ = Box.prototype;
  3796. Form.prototype.type = 'form';
  3797. Form.prototype._refresh = function() {
  3798. // XXX Possibly remove this if statement and refresh on every focus.
  3799. // Also potentially only include *visible* focusable elements.
  3800. // This would remove the need to check for _selected.visible in previous()
  3801. // and next().
  3802. if (!this._children) {
  3803. var out = [];
  3804. this.children.forEach(function fn(el) {
  3805. if (el.keyable) out.push(el);
  3806. el.children.forEach(fn);
  3807. });
  3808. this._children = out;
  3809. }
  3810. };
  3811. Form.prototype._visible = function() {
  3812. return !!this._children.filter(function(el) {
  3813. return el.visible;
  3814. }).length;
  3815. };
  3816. Form.prototype.next = function() {
  3817. this._refresh();
  3818. if (!this._visible()) return;
  3819. if (!this._selected) {
  3820. this._selected = this._children[0];
  3821. if (!this._selected.visible) return this.next();
  3822. // return this._selected;
  3823. if (this.screen.focused !== this._selected) return this._selected;
  3824. }
  3825. var i = this._children.indexOf(this._selected);
  3826. if (!~i || !this._children[i + 1]) {
  3827. this._selected = this._children[0];
  3828. if (!this._selected.visible) return this.next();
  3829. return this._selected;
  3830. }
  3831. this._selected = this._children[i + 1];
  3832. if (!this._selected.visible) return this.next();
  3833. return this._selected;
  3834. };
  3835. Form.prototype.previous = function() {
  3836. this._refresh();
  3837. if (!this._visible()) return;
  3838. if (!this._selected) {
  3839. this._selected = this._children[this._children.length - 1];
  3840. if (!this._selected.visible) return this.previous();
  3841. // return this._selected;
  3842. if (this.screen.focused !== this._selected) return this._selected;
  3843. }
  3844. var i = this._children.indexOf(this._selected);
  3845. if (!~i || !this._children[i - 1]) {
  3846. this._selected = this._children[this._children.length - 1];
  3847. if (!this._selected.visible) return this.previous();
  3848. return this._selected;
  3849. }
  3850. this._selected = this._children[i - 1];
  3851. if (!this._selected.visible) return this.previous();
  3852. return this._selected;
  3853. };
  3854. Form.prototype.focusNext = function() {
  3855. var next = this.next();
  3856. if (next) next.focus();
  3857. };
  3858. Form.prototype.focusPrevious = function() {
  3859. var previous = this.previous();
  3860. if (previous) previous.focus();
  3861. };
  3862. Form.prototype.resetSelected = function() {
  3863. this._selected = null;
  3864. };
  3865. Form.prototype.focusFirst = function() {
  3866. this.resetSelected();
  3867. this.focusNext();
  3868. };
  3869. Form.prototype.focusLast = function() {
  3870. this.resetSelected();
  3871. this.focusPrevious();
  3872. };
  3873. Form.prototype.submit = function() {
  3874. var self = this
  3875. , out = {};
  3876. this.children.forEach(function fn(el) {
  3877. if (el.value != null) {
  3878. var name = el.name || el.type;
  3879. if (Array.isArray(out[name])) {
  3880. out[name].push(el.value);
  3881. } else if (out[name]) {
  3882. out[name] = [out[name], el.value];
  3883. } else {
  3884. out[name] = el.value;
  3885. }
  3886. }
  3887. el.children.forEach(fn);
  3888. });
  3889. this.emit('submit', out);
  3890. return this.submission = out;
  3891. };
  3892. Form.prototype.cancel = function() {
  3893. this.emit('cancel');
  3894. };
  3895. Form.prototype.reset = function() {
  3896. this.children.forEach(function fn(el) {
  3897. switch (el.type) {
  3898. case 'screen':
  3899. break;
  3900. case 'box':
  3901. break;
  3902. case 'text':
  3903. break;
  3904. case 'line':
  3905. break;
  3906. case 'scrollable-box':
  3907. break;
  3908. case 'list':
  3909. el.select(0);
  3910. return;
  3911. case 'form':
  3912. break;
  3913. case 'input':
  3914. break;
  3915. case 'textbox':
  3916. el.clearInput();
  3917. return;
  3918. case 'textarea':
  3919. el.clearInput();
  3920. return;
  3921. case 'button':
  3922. delete el.value;
  3923. break;
  3924. case 'progress-bar':
  3925. el.setProgress(0);
  3926. break;
  3927. case 'file-manager':
  3928. el.refresh(el.options.cwd);
  3929. return;
  3930. case 'checkbox':
  3931. el.uncheck();
  3932. return;
  3933. case 'radio-set':
  3934. break;
  3935. case 'radio-button':
  3936. el.uncheck();
  3937. return;
  3938. case 'prompt':
  3939. break;
  3940. case 'question':
  3941. break;
  3942. case 'message':
  3943. break;
  3944. case 'info':
  3945. break;
  3946. case 'loading':
  3947. break;
  3948. case 'list-bar':
  3949. //el.select(0);
  3950. break;
  3951. case 'dir-manager':
  3952. el.refresh(el.options.cwd);
  3953. return;
  3954. case 'passbox':
  3955. el.clearInput();
  3956. return;
  3957. }
  3958. el.children.forEach(fn);
  3959. });
  3960. this.emit('reset');
  3961. };
  3962. /**
  3963. * Input
  3964. */
  3965. function Input(options) {
  3966. if (!(this instanceof Node)) {
  3967. return new Input(options);
  3968. }
  3969. options = options || {};
  3970. Box.call(this, options);
  3971. }
  3972. Input.prototype.__proto__ = Box.prototype;
  3973. Input.prototype.type = 'input';
  3974. /**
  3975. * Textarea
  3976. */
  3977. function Textarea(options) {
  3978. var self = this;
  3979. if (!(this instanceof Node)) {
  3980. return new Textarea(options);
  3981. }
  3982. options = options || {};
  3983. options.scrollable = options.scrollable !== false;
  3984. Input.call(this, options);
  3985. this.screen._listenKeys(this);
  3986. this.value = options.value || '';
  3987. //this.on('prerender', this._render.bind(this));
  3988. this.__updateCursor = this._updateCursor.bind(this);
  3989. this.on('resize', this.__updateCursor);
  3990. this.on('move', this.__updateCursor);
  3991. if (options.inputOnFocus) {
  3992. this.on('focus', this.readInput.bind(this, null));
  3993. }
  3994. if (!options.inputOnFocus && options.keys) {
  3995. this.on('keypress', function(ch, key) {
  3996. if (self._reading) return;
  3997. if (key.name === 'enter' || (options.vi && key.name === 'i')) {
  3998. return self.readInput();
  3999. }
  4000. if (key.name === 'e') {
  4001. return self.readEditor();
  4002. }
  4003. });
  4004. }
  4005. if (options.mouse) {
  4006. this.on('click', function(data) {
  4007. if (self._reading) return;
  4008. if (data.button !== 'right') return;
  4009. self.readEditor();
  4010. });
  4011. }
  4012. }
  4013. Textarea.prototype.__proto__ = Input.prototype;
  4014. Textarea.prototype.type = 'textarea';
  4015. Textarea.prototype._updateCursor = function(get) {
  4016. if (this.screen.focused !== this) {
  4017. return;
  4018. }
  4019. var lpos = get ? this.lpos : this._getCoords();
  4020. if (!lpos) return;
  4021. var last = this._clines[this._clines.length-1]
  4022. , program = this.screen.program
  4023. , line
  4024. , cx
  4025. , cy;
  4026. // Stop a situation where the textarea begins scrolling
  4027. // and the last cline appears to always be empty from the
  4028. // _typeScroll `+ '\n'` thing.
  4029. // Maybe not necessary anymore?
  4030. if (last === '' && this.value[this.value.length-1] !== '\n') {
  4031. last = this._clines[this._clines.length-2] || '';
  4032. }
  4033. line = Math.min(
  4034. this._clines.length - 1 - (this.childBase || 0),
  4035. (lpos.yl - lpos.yi) - this.iheight - 1);
  4036. // When calling clearValue() on a full textarea with a border, the first
  4037. // argument in the above Math.min call ends up being -2. Make sure we stay
  4038. // positive.
  4039. line = Math.max(0, line);
  4040. cy = lpos.yi + this.itop + line;
  4041. cx = lpos.xi + this.ileft + last.length;
  4042. // XXX Not sure, but this may still sometimes
  4043. // cause problems when leaving editor.
  4044. if (cy === program.y && cx === program.x) {
  4045. return;
  4046. }
  4047. if (cy === program.y) {
  4048. if (cx > program.x) {
  4049. program.cuf(cx - program.x);
  4050. } else if (cx < program.x) {
  4051. program.cub(program.x - cx);
  4052. }
  4053. } else if (cx === program.x) {
  4054. if (cy > program.y) {
  4055. program.cud(cy - program.y);
  4056. } else if (cy < program.y) {
  4057. program.cuu(program.y - cy);
  4058. }
  4059. } else {
  4060. program.cup(cy, cx);
  4061. }
  4062. };
  4063. Textarea.prototype.input =
  4064. Textarea.prototype.setInput =
  4065. Textarea.prototype.readInput = function(callback) {
  4066. var self = this
  4067. , focused = this.screen.focused === this;
  4068. if (this._reading) return;
  4069. this._reading = true;
  4070. this._callback = callback;
  4071. if (!focused) {
  4072. this.screen.saveFocus();
  4073. this.focus();
  4074. }
  4075. this.screen.grabKeys = true;
  4076. this._updateCursor();
  4077. this.screen.program.showCursor();
  4078. //this.screen.program.sgr('normal');
  4079. this._done = function fn(err, value) {
  4080. if (!self._reading) return;
  4081. if (fn.done) return;
  4082. fn.done = true;
  4083. self._reading = false;
  4084. delete self._callback;
  4085. delete self._done;
  4086. self.removeListener('keypress', self.__listener);
  4087. delete self.__listener;
  4088. self.removeListener('blur', self.__done);
  4089. delete self.__done;
  4090. self.screen.program.hideCursor();
  4091. self.screen.grabKeys = false;
  4092. if (!focused) {
  4093. self.screen.restoreFocus();
  4094. }
  4095. if (self.options.inputOnFocus) {
  4096. self.screen.rewindFocus();
  4097. }
  4098. // Ugly
  4099. if (err === 'stop') return;
  4100. if (err) {
  4101. self.emit('error', err);
  4102. } else if (value != null) {
  4103. self.emit('submit', value);
  4104. } else {
  4105. self.emit('cancel', value);
  4106. }
  4107. self.emit('action', value);
  4108. if (!callback) return;
  4109. return err
  4110. ? callback(err)
  4111. : callback(null, value);
  4112. };
  4113. this.__listener = this._listener.bind(this);
  4114. this.on('keypress', this.__listener);
  4115. this.__done = this._done.bind(this, null, null);
  4116. this.on('blur', this.__done);
  4117. };
  4118. Textarea.prototype._listener = function(ch, key) {
  4119. var done = this._done
  4120. , value = this.value;
  4121. if (key.name === 'return') return;
  4122. if (key.name === 'enter') {
  4123. ch = '\n';
  4124. }
  4125. // TODO: Handle directional keys.
  4126. if (key.name === 'left' || key.name === 'right'
  4127. || key.name === 'up' || key.name === 'down') {
  4128. ;
  4129. }
  4130. if (this.options.keys && key.ctrl && key.name === 'e') {
  4131. return this.readEditor();
  4132. }
  4133. // TODO: Optimize typing by writing directly
  4134. // to the screen and screen buffer here.
  4135. if (key.name === 'escape') {
  4136. done(null, null);
  4137. } else if (key.name === 'backspace') {
  4138. if (this.value.length) {
  4139. this.value = this.value.slice(0, -1);
  4140. }
  4141. } else if (ch) {
  4142. if (!/^[\x00-\x08\x0b-\x0c\x0e-\x1f\x7f]$/.test(ch)) {
  4143. this.value += ch;
  4144. }
  4145. }
  4146. if (this.value !== value) {
  4147. //this.setValue();
  4148. this.screen.render();
  4149. }
  4150. };
  4151. Textarea.prototype._typeScroll = function() {
  4152. // XXX Workaround
  4153. var height = this.height - this.iheight;
  4154. if (this._clines.length - this.childBase > height) {
  4155. //this.setContent(this.value + '\n');
  4156. this.scroll(this._clines.length);
  4157. }
  4158. };
  4159. Textarea.prototype.getValue = function() {
  4160. return this.value;
  4161. };
  4162. Textarea.prototype.setValue = function(value) {
  4163. if (value == null) {
  4164. value = this.value;
  4165. }
  4166. if (this._value !== value) {
  4167. this.value = value;
  4168. this._value = value;
  4169. this.setContent(this.value);
  4170. this._typeScroll();
  4171. this._updateCursor();
  4172. }
  4173. };
  4174. Textarea.prototype.clearInput =
  4175. Textarea.prototype.clearValue = function() {
  4176. return this.setValue('');
  4177. };
  4178. Textarea.prototype.submit = function() {
  4179. if (!this.__listener) return;
  4180. return this.__listener('\x1b', { name: 'escape' });
  4181. };
  4182. Textarea.prototype.cancel = function() {
  4183. if (!this.__listener) return;
  4184. return this.__listener('\x1b', { name: 'escape' });
  4185. };
  4186. Textarea.prototype.render = function() {
  4187. this.setValue();
  4188. return this._render();
  4189. };
  4190. Textarea.prototype.editor =
  4191. Textarea.prototype.setEditor =
  4192. Textarea.prototype.readEditor = function(callback) {
  4193. var self = this;
  4194. if (this._reading) {
  4195. var _cb = this._callback
  4196. , cb = callback;
  4197. this._done('stop');
  4198. callback = function(err, value) {
  4199. if (_cb) _cb(err, value);
  4200. if (cb) cb(err, value);
  4201. };
  4202. }
  4203. if (!callback) {
  4204. callback = function() {};
  4205. }
  4206. return this.screen.readEditor({ value: this.value }, function(err, value) {
  4207. if (err) {
  4208. if (err.message === 'Unsuccessful.') {
  4209. self.screen.render();
  4210. return self.readInput(callback);
  4211. }
  4212. self.screen.render();
  4213. self.readInput(callback);
  4214. return callback(err);
  4215. }
  4216. self.setValue(value);
  4217. self.screen.render();
  4218. return self.readInput(callback);
  4219. });
  4220. };
  4221. /**
  4222. * Textbox
  4223. */
  4224. function Textbox(options) {
  4225. var self = this;
  4226. if (!(this instanceof Node)) {
  4227. return new Textbox(options);
  4228. }
  4229. options = options || {};
  4230. options.scrollable = false;
  4231. Textarea.call(this, options);
  4232. this.secret = options.secret;
  4233. this.censor = options.censor;
  4234. }
  4235. Textbox.prototype.__proto__ = Textarea.prototype;
  4236. Textbox.prototype.type = 'textbox';
  4237. Textbox.prototype.__olistener = Textbox.prototype._listener;
  4238. Textbox.prototype._listener = function(ch, key) {
  4239. if (key.name === 'enter') {
  4240. this._done(null, this.value);
  4241. return;
  4242. }
  4243. return this.__olistener(ch, key);
  4244. };
  4245. Textbox.prototype.setValue = function(value) {
  4246. var visible, val;
  4247. if (value == null) {
  4248. value = this.value;
  4249. }
  4250. if (this._value !== value) {
  4251. value = value.replace(/\n/g, '');
  4252. this.value = value;
  4253. this._value = value;
  4254. if (this.secret) {
  4255. this.setContent('');
  4256. } else if (this.censor) {
  4257. this.setContent(Array(this.value.length + 1).join('*'));
  4258. } else {
  4259. visible = -(this.width - this.iwidth - 1);
  4260. val = this.value.replace(/\t/g, this.screen.tabc);
  4261. this.setContent(val.slice(visible));
  4262. }
  4263. this._updateCursor();
  4264. }
  4265. };
  4266. Textbox.prototype.submit = function() {
  4267. if (!this.__listener) return;
  4268. return this.__listener('\r', { name: 'enter' });
  4269. };
  4270. /**
  4271. * Button
  4272. */
  4273. function Button(options) {
  4274. var self = this;
  4275. if (!(this instanceof Node)) {
  4276. return new Button(options);
  4277. }
  4278. options = options || {};
  4279. if (options.autoFocus == null) {
  4280. options.autoFocus = false;
  4281. }
  4282. Input.call(this, options);
  4283. this.on('keypress', function(ch, key) {
  4284. if (key.name === 'enter' || key.name === 'space') {
  4285. return self.press();
  4286. }
  4287. });
  4288. if (this.options.mouse) {
  4289. this.on('click', function() {
  4290. return self.press();
  4291. });
  4292. }
  4293. }
  4294. Button.prototype.__proto__ = Input.prototype;
  4295. Button.prototype.type = 'button';
  4296. Button.prototype.press = function() {
  4297. this.value = true;
  4298. var result = this.emit('press');
  4299. delete this.value;
  4300. return result;
  4301. };
  4302. /**
  4303. * ProgressBar
  4304. */
  4305. function ProgressBar(options) {
  4306. var self = this;
  4307. if (!(this instanceof Node)) {
  4308. return new ProgressBar(options);
  4309. }
  4310. options = options || {};
  4311. Input.call(this, options);
  4312. this.filled = options.filled || 0;
  4313. if (typeof this.filled === 'string') {
  4314. this.filled = +this.filled.slice(0, -1);
  4315. }
  4316. this.value = this.filled;
  4317. this.ch = options.ch || ' ';
  4318. if (!this.style.bar) {
  4319. this.style.bar = {};
  4320. this.style.bar.fg = options.barFg;
  4321. this.style.bar.bg = options.barBg;
  4322. }
  4323. this.orientation = options.orientation || 'horizontal';
  4324. if (options.keys) {
  4325. this.on('keypress', function(ch, key) {
  4326. var back, forward;
  4327. if (self.orientation === 'horizontal') {
  4328. back = ['left', 'h'];
  4329. forward = ['right', 'l'];
  4330. } else if (self.orientation === 'vertical') {
  4331. back = ['down', 'j'];
  4332. forward = ['up', 'k'];
  4333. }
  4334. if (key.name === back[0] || (options.vi && key.name === back[1])) {
  4335. self.progress(-5);
  4336. self.screen.render();
  4337. return;
  4338. }
  4339. if (key.name === forward[0] || (options.vi && key.name === forward[1])) {
  4340. self.progress(5);
  4341. self.screen.render();
  4342. return;
  4343. }
  4344. });
  4345. }
  4346. if (options.mouse) {
  4347. this.on('click', function(data) {
  4348. var x, y, m, p;
  4349. if (!self.lpos) return;
  4350. if (self.orientation === 'horizontal') {
  4351. x = data.x - self.lpos.xi;
  4352. m = (self.lpos.xl - self.lpos.xi) - self.iwidth;
  4353. p = x / m * 100 | 0;
  4354. } else if (self.orientation === 'vertical') {
  4355. y = data.y - self.lpos.yi;
  4356. m = (self.lpos.yl - self.lpos.yi) - self.iheight;
  4357. p = y / m * 100 | 0;
  4358. }
  4359. self.setProgress(p);
  4360. });
  4361. }
  4362. //this.on('render', this._render.bind(this));
  4363. }
  4364. ProgressBar.prototype.__proto__ = Input.prototype;
  4365. ProgressBar.prototype.type = 'progress-bar';
  4366. ProgressBar.prototype.render = function() {
  4367. var ret = this._render();
  4368. if (!ret) return;
  4369. var xi = ret.xi
  4370. , xl = ret.xl
  4371. , yi = ret.yi
  4372. , yl = ret.yl
  4373. , dattr;
  4374. if (this.border) xi++, yi++, xl--, yl--;
  4375. if (this.orientation === 'horizontal') {
  4376. xl = xi + ((xl - xi) * (this.filled / 100)) | 0;
  4377. } else if (this.orientation === 'vertical') {
  4378. yi = yi + ((yl - yi) - (((yl - yi) * (this.filled / 100)) | 0));
  4379. }
  4380. dattr = this.sattr(this, this.style.bar.fg, this.style.bar.bg);
  4381. this.screen.fillRegion(dattr, this.ch, xi, xl, yi, yl);
  4382. if (this.content) {
  4383. var line = this.screen.lines[yi];
  4384. for (var i = 0; i < this.content.length; i++) {
  4385. line[xi + i][1] = this.content[i];
  4386. }
  4387. line.dirty = true;
  4388. }
  4389. return ret;
  4390. };
  4391. ProgressBar.prototype.progress = function(filled) {
  4392. this.filled += filled;
  4393. if (this.filled < 0) this.filled = 0;
  4394. else if (this.filled > 100) this.filled = 100;
  4395. if (this.filled === 100) {
  4396. this.emit('complete');
  4397. }
  4398. this.value = this.filled;
  4399. };
  4400. ProgressBar.prototype.setProgress = function(filled) {
  4401. this.filled = 0;
  4402. this.progress(filled);
  4403. };
  4404. ProgressBar.prototype.reset = function() {
  4405. this.emit('reset');
  4406. this.filled = 0;
  4407. this.value = this.filled;
  4408. };
  4409. /**
  4410. * FileManager
  4411. */
  4412. function FileManager(options) {
  4413. var self = this;
  4414. if (!(this instanceof Node)) {
  4415. return new FileManager(options);
  4416. }
  4417. options = options || {};
  4418. options.parseTags = true;
  4419. // options.label = ' {blue-fg}%path{/blue-fg} ';
  4420. List.call(this, options);
  4421. this.cwd = options.cwd || process.cwd();
  4422. this.file = this.cwd;
  4423. this.value = this.cwd;
  4424. if (options.label && ~options.label.indexOf('%path')) {
  4425. this._label.setContent(options.label.replace('%path', this.cwd));
  4426. }
  4427. this.on('select', function(item) {
  4428. var value = item.content.replace(/\{[^{}]+\}/g, '').replace(/@$/, '')
  4429. , file = path.resolve(self.cwd, value);
  4430. return fs.stat(file, function(err, stat) {
  4431. if (err) {
  4432. return self.emit('error', err, file);
  4433. }
  4434. self.file = file;
  4435. self.value = file;
  4436. if (stat.isDirectory()) {
  4437. self.emit('cd', file, self.cwd);
  4438. self.cwd = file;
  4439. if (options.label && ~options.label.indexOf('%path')) {
  4440. self._label.setContent(options.label.replace('%path', file));
  4441. }
  4442. self.refresh();
  4443. } else {
  4444. self.emit('file', file);
  4445. }
  4446. });
  4447. });
  4448. }
  4449. FileManager.prototype.__proto__ = List.prototype;
  4450. FileManager.prototype.type = 'file-manager';
  4451. FileManager.prototype.refresh = function(cwd, callback) {
  4452. if (!callback) {
  4453. callback = cwd;
  4454. cwd = null;
  4455. }
  4456. var self = this;
  4457. if (cwd) this.cwd = cwd;
  4458. else cwd = this.cwd;
  4459. return fs.readdir(cwd, function(err, list) {
  4460. if (err && err.code === 'ENOENT') {
  4461. self.cwd = cwd !== process.env.HOME
  4462. ? process.env.HOME
  4463. : '/';
  4464. return self.refresh(callback);
  4465. }
  4466. if (err) {
  4467. if (callback) return callback(err);
  4468. return self.emit('error', err, cwd);
  4469. }
  4470. var dirs = []
  4471. , files = [];
  4472. list.unshift('..');
  4473. list.forEach(function(name) {
  4474. var f = path.resolve(cwd, name)
  4475. , stat;
  4476. try {
  4477. stat = fs.lstatSync(f);
  4478. } catch (e) {
  4479. ;
  4480. }
  4481. if ((stat && stat.isDirectory()) || name === '..') {
  4482. dirs.push({
  4483. name: name,
  4484. text: '{light-blue-fg}' + name + '{/light-blue-fg}/',
  4485. dir: true
  4486. });
  4487. } else if (stat && stat.isSymbolicLink()) {
  4488. files.push({
  4489. name: name,
  4490. text: '{light-cyan-fg}' + name + '{/light-cyan-fg}@',
  4491. dir: false
  4492. });
  4493. } else {
  4494. files.push({
  4495. name: name,
  4496. text: name,
  4497. dir: false
  4498. });
  4499. }
  4500. });
  4501. dirs = asort(dirs);
  4502. files = asort(files);
  4503. list = dirs.concat(files).map(function(data) {
  4504. return data.text;
  4505. });
  4506. self.setItems(list);
  4507. self.select(0);
  4508. self.screen.render();
  4509. if (callback) callback();
  4510. });
  4511. };
  4512. FileManager.prototype.pick = function(cwd, callback) {
  4513. if (!callback) {
  4514. callback = cwd;
  4515. cwd = null;
  4516. }
  4517. var self = this
  4518. , focused = this.screen.focused === this
  4519. , hidden = this.hidden
  4520. , onfile
  4521. , oncancel;
  4522. function resume() {
  4523. self.removeListener('file', onfile);
  4524. self.removeListener('cancel', oncancel);
  4525. if (hidden) {
  4526. self.hide();
  4527. }
  4528. if (!focused) {
  4529. self.screen.restoreFocus();
  4530. }
  4531. self.screen.render();
  4532. }
  4533. this.on('file', onfile = function(file) {
  4534. resume();
  4535. return callback(null, file);
  4536. });
  4537. this.on('cancel', oncancel = function() {
  4538. resume();
  4539. return callback();
  4540. });
  4541. this.refresh(cwd, function(err) {
  4542. if (err) return callback(err);
  4543. if (hidden) {
  4544. self.show();
  4545. }
  4546. if (!focused) {
  4547. self.screen.saveFocus();
  4548. self.focus();
  4549. }
  4550. self.screen.render();
  4551. });
  4552. };
  4553. FileManager.prototype.reset = function(cwd, callback) {
  4554. if (!callback) {
  4555. callback = cwd;
  4556. cwd = null;
  4557. }
  4558. this.cwd = cwd || this.options.cwd;
  4559. this.refresh(callback);
  4560. };
  4561. /**
  4562. * Checkbox
  4563. */
  4564. function Checkbox(options) {
  4565. var self = this;
  4566. if (!(this instanceof Node)) {
  4567. return new Checkbox(options);
  4568. }
  4569. options = options || {};
  4570. Input.call(this, options);
  4571. this.text = options.content || options.text || '';
  4572. this.checked = this.value = options.checked || false;
  4573. this.on('keypress', function(ch, key) {
  4574. if (key.name === 'enter' || key.name === 'space') {
  4575. self.toggle();
  4576. self.screen.render();
  4577. }
  4578. });
  4579. if (options.mouse) {
  4580. this.on('click', function() {
  4581. self.toggle();
  4582. self.screen.render();
  4583. });
  4584. }
  4585. this.on('focus', function(old) {
  4586. var lpos = self.lpos;
  4587. if (!lpos) return;
  4588. //self.screen.program.saveCursor();
  4589. self.screen.program.lsaveCursor('checkbox');
  4590. self.screen.program.cup(lpos.yi, lpos.xi + 1);
  4591. self.screen.program.showCursor();
  4592. });
  4593. this.on('blur', function() {
  4594. //self.screen.program.hideCursor();
  4595. //self.screen.program.restoreCursor();
  4596. self.screen.program.lrestoreCursor('checkbox', true);
  4597. });
  4598. }
  4599. Checkbox.prototype.__proto__ = Input.prototype;
  4600. Checkbox.prototype.type = 'checkbox';
  4601. Checkbox.prototype.render = function() {
  4602. this.clearPos(true);
  4603. this.setContent('[' + (this.checked ? 'x' : ' ') + '] ' + this.text, true);
  4604. return this._render();
  4605. };
  4606. Checkbox.prototype.check = function() {
  4607. if (this.checked) return;
  4608. this.checked = this.value = true;
  4609. this.emit('check');
  4610. };
  4611. Checkbox.prototype.uncheck = function() {
  4612. if (!this.checked) return;
  4613. this.checked = this.value = false;
  4614. this.emit('uncheck');
  4615. };
  4616. Checkbox.prototype.toggle = function() {
  4617. return this.checked
  4618. ? this.uncheck()
  4619. : this.check();
  4620. };
  4621. /**
  4622. * RadioSet
  4623. */
  4624. function RadioSet(options) {
  4625. if (!(this instanceof Node)) {
  4626. return new RadioSet(options);
  4627. }
  4628. options = options || {};
  4629. // Possibly inherit parent's style.
  4630. // options.style = this.parent.style;
  4631. Box.call(this, options);
  4632. }
  4633. RadioSet.prototype.__proto__ = Box.prototype;
  4634. RadioSet.prototype.type = 'radio-set';
  4635. /**
  4636. * RadioButton
  4637. */
  4638. function RadioButton(options) {
  4639. var self = this;
  4640. if (!(this instanceof Node)) {
  4641. return new RadioButton(options);
  4642. }
  4643. options = options || {};
  4644. Checkbox.call(this, options);
  4645. this.on('check', function() {
  4646. var el = self;
  4647. while (el = el.parent) {
  4648. if (el.type === 'radio-set'
  4649. || el.type === 'form') break;
  4650. }
  4651. el = el || self.parent;
  4652. el.forDescendants(function(el) {
  4653. if (el.type !== 'radio-button' || el === self) {
  4654. return;
  4655. }
  4656. el.uncheck();
  4657. });
  4658. });
  4659. }
  4660. RadioButton.prototype.__proto__ = Checkbox.prototype;
  4661. RadioButton.prototype.type = 'radio-button';
  4662. RadioButton.prototype.render = function() {
  4663. this.clearPos(true);
  4664. this.setContent('(' + (this.checked ? '*' : ' ') + ') ' + this.text, true);
  4665. return this._render();
  4666. };
  4667. RadioButton.prototype.toggle = RadioButton.prototype.check;
  4668. /**
  4669. * Prompt
  4670. */
  4671. function Prompt(options) {
  4672. var self = this;
  4673. if (!(this instanceof Node)) {
  4674. return new Prompt(options);
  4675. }
  4676. options = options || {};
  4677. options.hidden = true;
  4678. Box.call(this, options);
  4679. this._.input = new Textbox({
  4680. parent: this,
  4681. top: 3,
  4682. height: 1,
  4683. left: 2,
  4684. right: 2,
  4685. bg: 'black'
  4686. });
  4687. this._.okay = new Button({
  4688. parent: this,
  4689. top: 5,
  4690. height: 1,
  4691. left: 2,
  4692. width: 6,
  4693. content: 'Okay',
  4694. align: 'center',
  4695. bg: 'black',
  4696. hoverBg: 'blue',
  4697. autoFocus: false,
  4698. mouse: true
  4699. });
  4700. this._.cancel = new Button({
  4701. parent: this,
  4702. top: 5,
  4703. height: 1,
  4704. shrink: true,
  4705. left: 10,
  4706. width: 8,
  4707. content: 'Cancel',
  4708. align: 'center',
  4709. bg: 'black',
  4710. hoverBg: 'blue',
  4711. autoFocus: false,
  4712. mouse: true
  4713. });
  4714. }
  4715. Prompt.prototype.__proto__ = Box.prototype;
  4716. Prompt.prototype.type = 'prompt';
  4717. Prompt.prototype.type = function(text, value, callback) {
  4718. var self = this;
  4719. var okay, cancel;
  4720. if (!callback) {
  4721. callback = value;
  4722. value = '';
  4723. }
  4724. //var parent = this.parent;
  4725. //this.detach();
  4726. //parent.append(this);
  4727. this.show();
  4728. this.setContent(' ' + text);
  4729. this._.input.value = value;
  4730. this.screen.saveFocus();
  4731. this._.okay.on('press', okay = function() {
  4732. self._.input.submit();
  4733. });
  4734. this._.cancel.on('press', cancel = function() {
  4735. self._.input.cancel();
  4736. });
  4737. this._.input.readInput(function(err, data) {
  4738. self.hide();
  4739. self.screen.restoreFocus();
  4740. self._.okay.removeListener('press', okay);
  4741. self._.cancel.removeListener('press', cancel);
  4742. return callback(err, data);
  4743. });
  4744. this.screen.render();
  4745. };
  4746. /**
  4747. * Question
  4748. */
  4749. function Question(options) {
  4750. var self = this;
  4751. if (!(this instanceof Node)) {
  4752. return new Question(options);
  4753. }
  4754. options = options || {};
  4755. options.hidden = true;
  4756. Box.call(this, options);
  4757. this._.okay = new Button({
  4758. screen: this.screen,
  4759. parent: this,
  4760. top: 2,
  4761. height: 1,
  4762. left: 2,
  4763. width: 6,
  4764. content: 'Okay',
  4765. align: 'center',
  4766. bg: 'black',
  4767. hoverBg: 'blue',
  4768. autoFocus: false,
  4769. mouse: true
  4770. });
  4771. this._.cancel = new Button({
  4772. screen: this.screen,
  4773. parent: this,
  4774. top: 2,
  4775. height: 1,
  4776. shrink: true,
  4777. left: 10,
  4778. width: 8,
  4779. content: 'Cancel',
  4780. align: 'center',
  4781. bg: 'black',
  4782. hoverBg: 'blue',
  4783. autoFocus: false,
  4784. mouse: true
  4785. });
  4786. }
  4787. Question.prototype.__proto__ = Box.prototype;
  4788. Question.prototype.type = 'question';
  4789. Question.prototype.ask = function(text, callback) {
  4790. var self = this;
  4791. var press, okay, cancel;
  4792. //var parent = this.parent;
  4793. //this.detach();
  4794. //parent.append(this);
  4795. this.show();
  4796. this.setContent(' ' + text);
  4797. this.screen.on('keypress', press = function(ch, key) {
  4798. if (key.name === 'mouse') return;
  4799. if (key.name !== 'enter'
  4800. && key.name !== 'escape'
  4801. && key.name !== 'q'
  4802. && key.name !== 'y'
  4803. && key.name !== 'n') {
  4804. return;
  4805. }
  4806. done(null, key.name === 'enter' || key.name === 'y');
  4807. });
  4808. this._.okay.on('press', okay = function() {
  4809. done(null, true);
  4810. });
  4811. this._.cancel.on('press', cancel = function() {
  4812. done(null, false);
  4813. });
  4814. this.screen.saveFocus();
  4815. this.focus();
  4816. function done(err, data) {
  4817. self.hide();
  4818. self.screen.restoreFocus();
  4819. self.screen.removeListener('keypress', press);
  4820. self._.okay.removeListener('press', okay);
  4821. self._.cancel.removeListener('press', cancel);
  4822. return callback(err, data);
  4823. }
  4824. this.screen.render();
  4825. };
  4826. /**
  4827. * Message / Error
  4828. */
  4829. function Message(options) {
  4830. var self = this;
  4831. if (!(this instanceof Node)) {
  4832. return new Message(options);
  4833. }
  4834. options = options || {};
  4835. options.tags = true;
  4836. Box.call(this, options);
  4837. }
  4838. Message.prototype.__proto__ = Box.prototype;
  4839. Message.prototype.type = 'message';
  4840. Message.prototype.log =
  4841. Message.prototype.display = function(text, time, callback) {
  4842. var self = this;
  4843. if (typeof time === 'function') {
  4844. callback = time;
  4845. time = null;
  4846. }
  4847. if (time == null) time = 3;
  4848. //time = time || 3;
  4849. //var parent = this.parent;
  4850. //this.detach();
  4851. //parent.append(this);
  4852. if (this.scrollable) {
  4853. this.screen.saveFocus();
  4854. this.focus();
  4855. this.setScroll(0);
  4856. }
  4857. this.show();
  4858. this.setContent(text);
  4859. this.screen.render();
  4860. if (time === Infinity || time === -1 || time === 0) {
  4861. var end = function() {
  4862. if (end.done) return;
  4863. end.done = true;
  4864. if (self.scrollable) {
  4865. try {
  4866. self.screen.restoreFocus();
  4867. } catch (e) {
  4868. ;
  4869. }
  4870. }
  4871. self.hide();
  4872. self.screen.render();
  4873. if (callback) callback();
  4874. };
  4875. setTimeout(function() {
  4876. self.screen.on('keypress', function fn(ch, key) {
  4877. if (key.name === 'mouse') return;
  4878. if (self.scrollable) {
  4879. if ((key.name === 'up' || (self.options.vi && key.name === 'k'))
  4880. || (key.name === 'down' || (self.options.vi && key.name === 'j'))
  4881. || (self.options.vi && key.name === 'u' && key.ctrl)
  4882. || (self.options.vi && key.name === 'd' && key.ctrl)
  4883. || (self.options.vi && key.name === 'b' && key.ctrl)
  4884. || (self.options.vi && key.name === 'f' && key.ctrl)
  4885. || (self.options.vi && key.name === 'g' && !key.shift)
  4886. || (self.options.vi && key.name === 'g' && key.shift)) {
  4887. return;
  4888. }
  4889. }
  4890. if (self.options.ignoreKeys && ~self.options.ignoreKeys.indexOf(key.name)) {
  4891. return;
  4892. }
  4893. self.screen.removeListener('keypress', fn);
  4894. end();
  4895. });
  4896. if (!self.options.mouse) return;
  4897. self.screen.on('mouse', function fn(data) {
  4898. if (data.action === 'mousemove') return;
  4899. self.screen.removeListener('mouse', fn);
  4900. end();
  4901. });
  4902. }, 10);
  4903. return;
  4904. }
  4905. setTimeout(function() {
  4906. self.hide();
  4907. self.screen.render();
  4908. if (callback) callback();
  4909. }, time * 1000);
  4910. };
  4911. Message.prototype.error = function(text, time, callback) {
  4912. return this.display('{red-fg}Error: ' + text + '{/red-fg}', time, callback);
  4913. };
  4914. /**
  4915. * Loading
  4916. */
  4917. function Loading(options) {
  4918. var self = this;
  4919. if (!(this instanceof Node)) {
  4920. return new Loading(options);
  4921. }
  4922. options = options || {};
  4923. Box.call(this, options);
  4924. this._.icon = new Text({
  4925. parent: this,
  4926. align: 'center',
  4927. top: 2,
  4928. left: 1,
  4929. right: 1,
  4930. height: 1,
  4931. content: '|'
  4932. });
  4933. }
  4934. Loading.prototype.__proto__ = Box.prototype;
  4935. Loading.prototype.type = 'loading';
  4936. Loading.prototype.load = function(text) {
  4937. var self = this;
  4938. //var parent = this.parent;
  4939. //this.detach();
  4940. //parent.append(this);
  4941. this.show();
  4942. this.setContent(text);
  4943. if (this._.timer) {
  4944. this.stop();
  4945. }
  4946. this.screen.lockKeys = true;
  4947. this._.timer = setInterval(function() {
  4948. if (self._.icon.content === '|') {
  4949. self._.icon.setContent('/');
  4950. } else if (self._.icon.content === '/') {
  4951. self._.icon.setContent('-');
  4952. } else if (self._.icon.content === '-') {
  4953. self._.icon.setContent('\\');
  4954. } else if (self._.icon.content === '\\') {
  4955. self._.icon.setContent('|');
  4956. }
  4957. self.screen.render();
  4958. }, 200);
  4959. };
  4960. Loading.prototype.stop = function() {
  4961. this.screen.lockKeys = false;
  4962. this.hide();
  4963. if (this._.timer) {
  4964. clearInterval(this._.timer);
  4965. delete this._.timer;
  4966. }
  4967. this.screen.render();
  4968. };
  4969. /**
  4970. * Listbar / HorizontalList
  4971. */
  4972. function Listbar(options) {
  4973. var self = this;
  4974. if (!(this instanceof Node)) {
  4975. return new Listbar(options);
  4976. }
  4977. options = options || {};
  4978. // XXX Workaround to make sure buttons don't
  4979. // overlap border on the right.
  4980. // options.scrollable = true;
  4981. this.items = [];
  4982. this.ritems = [];
  4983. this.commands = [];
  4984. this.leftBase = 0;
  4985. this.leftOffset = 0;
  4986. this.mouse = options.mouse || false;
  4987. Box.call(this, options);
  4988. //this._.debug = new Box({
  4989. // parent: this.screen,
  4990. // top: 0,
  4991. // left: 0,
  4992. // height: 'shrink',
  4993. // width: 'shrink',
  4994. // content: '...'
  4995. //});
  4996. if (options.commands || options.items) {
  4997. this.setItems(options.commands || options.items);
  4998. }
  4999. if (options.keys) {
  5000. this.on('keypress', function(ch, key) {
  5001. if (key.name === 'left'
  5002. || (options.vi && key.name === 'h')
  5003. || (key.shift && key.name === 'tab')) {
  5004. self.moveLeft();
  5005. self.screen.render();
  5006. // Stop propagation if we're in a form.
  5007. if (key.name === 'tab') return false;
  5008. return;
  5009. }
  5010. if (key.name === 'right'
  5011. || (options.vi && key.name === 'l')
  5012. || key.name === 'tab') {
  5013. self.moveRight();
  5014. self.screen.render();
  5015. // Stop propagation if we're in a form.
  5016. if (key.name === 'tab') return false;
  5017. return;
  5018. }
  5019. if (key.name === 'enter'
  5020. || (options.vi && key.name === 'k' && !key.shift)) {
  5021. self.emit('action', self.items[self.selected], self.selected);
  5022. self.emit('select', self.items[self.selected], self.selected);
  5023. var item = self.items[self.selected];
  5024. //item.press();
  5025. if (item._.cmd.callback) {
  5026. item._.cmd.callback();
  5027. }
  5028. self.screen.render();
  5029. return;
  5030. }
  5031. if (key.name === 'escape' || (options.vi && key.name === 'q')) {
  5032. self.emit('action');
  5033. self.emit('cancel');
  5034. return;
  5035. }
  5036. });
  5037. }
  5038. if (options.autoCommandKeys) {
  5039. this.screen.on('keypress', function(ch, key) {
  5040. if (/^[0-9]$/.test(ch)) {
  5041. var i = +ch - 1;
  5042. if (!~i) i = 9;
  5043. return self.selectTab(i);
  5044. }
  5045. });
  5046. }
  5047. this.on('focus', function() {
  5048. self.select(self.selected);
  5049. });
  5050. }
  5051. Listbar.prototype.__proto__ = Box.prototype;
  5052. Listbar.prototype.type = 'listbar';
  5053. Listbar.prototype.__defineGetter__('selected', function() {
  5054. return this.leftBase + this.leftOffset;
  5055. });
  5056. Listbar.prototype.setItems = function(commands) {
  5057. var self = this;
  5058. if (!Array.isArray(commands)) {
  5059. commands = Object.keys(commands).reduce(function(obj, key, i) {
  5060. var cmd = commands[key]
  5061. , cb;
  5062. if (typeof cmd === 'function') {
  5063. cb = cmd;
  5064. cmd = { callback: cb };
  5065. }
  5066. if (cmd.text == null) cmd.text = key;
  5067. if (cmd.prefix == null) cmd.prefix = ++i + '';
  5068. if (cmd.text == null && cmd.callback) {
  5069. cmd.text = cmd.callback.name;
  5070. }
  5071. obj.push(cmd);
  5072. return obj;
  5073. }, []);
  5074. }
  5075. this.items.forEach(function(el) {
  5076. el.detach();
  5077. });
  5078. this.items = [];
  5079. this.ritems = [];
  5080. this.commands = [];
  5081. commands.forEach(function(cmd) {
  5082. self.add(cmd);
  5083. });
  5084. };
  5085. Listbar.prototype.add =
  5086. Listbar.prototype.addItem =
  5087. Listbar.prototype.appendItem = function(item, callback) {
  5088. var self = this
  5089. , prev = this.items[this.items.length - 1]
  5090. , drawn = prev ? prev.left + prev.width : 0
  5091. , cmd
  5092. , title
  5093. , len;
  5094. if (!this.screen.autoPadding) {
  5095. drawn += this.ileft;
  5096. }
  5097. if (typeof item === 'object') {
  5098. cmd = item;
  5099. if (cmd.prefix == null) cmd.prefix = (this.items.length + 1) + '';
  5100. }
  5101. if (typeof item === 'string') {
  5102. cmd = {
  5103. prefix: (this.items.length + 1) + '',
  5104. text: item,
  5105. callback: callback
  5106. };
  5107. }
  5108. if (typeof item === 'function') {
  5109. cmd = {
  5110. prefix: (this.items.length + 1) + '',
  5111. text: item.name,
  5112. callback: item
  5113. };
  5114. }
  5115. if (cmd.keys && cmd.keys[0]) {
  5116. cmd.prefix = cmd.keys[0];
  5117. }
  5118. var t = generateTags(this.style.prefix || { fg: 'lightblack' });
  5119. title = (cmd.prefix != null ? t.open + cmd.prefix + t.close + ':' : '') + cmd.text;
  5120. len = ((cmd.prefix != null ? cmd.prefix + ':' : '') + cmd.text).length;
  5121. var options = {
  5122. screen: this.screen,
  5123. top: 0,
  5124. left: drawn + 1,
  5125. height: 1,
  5126. content: title,
  5127. width: len + 2,
  5128. align: 'center',
  5129. autoFocus: false,
  5130. tags: true,
  5131. mouse: true,
  5132. style: merge({}, this.style.item),
  5133. noOverflow: true
  5134. };
  5135. if (!this.screen.autoPadding) {
  5136. options.top += this.itop;
  5137. options.left += this.ileft;
  5138. }
  5139. ['bg', 'fg', 'bold', 'underline',
  5140. 'blink', 'inverse', 'invisible'].forEach(function(name) {
  5141. options.style[name] = function() {
  5142. var attr = self.items[self.selected] === el
  5143. ? self.style.selected[name]
  5144. : self.style.item[name];
  5145. if (typeof attr === 'function') attr = attr(el);
  5146. return attr;
  5147. };
  5148. });
  5149. var el = new Box(options);
  5150. //var el = new Button(options);
  5151. this._[cmd.text] = el;
  5152. cmd.element = el;
  5153. el._.cmd = cmd;
  5154. this.ritems.push(cmd.text);
  5155. this.items.push(el);
  5156. this.commands.push(cmd);
  5157. this.append(el);
  5158. if (cmd.callback) {
  5159. //el.on('press', cmd.callback);
  5160. //this.on('select', function(el) {
  5161. // if (el._.cmd.callback) {
  5162. // el._.cmd.callback();
  5163. // }
  5164. //});
  5165. if (cmd.keys) {
  5166. this.screen.key(cmd.keys, function(ch, key) {
  5167. self.emit('action', el, self.selected);
  5168. self.emit('select', el, self.selected);
  5169. //el.press();
  5170. if (el._.cmd.callback) {
  5171. el._.cmd.callback();
  5172. }
  5173. self.select(el);
  5174. self.screen.render();
  5175. });
  5176. }
  5177. }
  5178. if (this.items.length === 1) {
  5179. this.select(0);
  5180. }
  5181. if (this.mouse) {
  5182. el.on('click', function(data) {
  5183. self.emit('action', el, self.selected);
  5184. self.emit('select', el, self.selected);
  5185. //el.press();
  5186. if (el._.cmd.callback) {
  5187. el._.cmd.callback();
  5188. }
  5189. self.select(el);
  5190. self.screen.render();
  5191. });
  5192. }
  5193. };
  5194. Listbar.prototype.render = function() {
  5195. var self = this
  5196. , drawn = 0;
  5197. if (!this.screen.autoPadding) {
  5198. drawn += this.ileft;
  5199. }
  5200. this.items.forEach(function(el, i) {
  5201. if (i < self.leftBase) {
  5202. el.hide();
  5203. } else {
  5204. el.rleft = drawn + 1;
  5205. drawn += el.width + 2;
  5206. el.show();
  5207. }
  5208. });
  5209. return this._render();
  5210. };
  5211. Listbar.prototype.select = function(offset) {
  5212. if (typeof offset !== 'number') {
  5213. offset = this.items.indexOf(offset);
  5214. }
  5215. var lpos = this._getCoords();
  5216. if (!lpos) return;
  5217. var self = this
  5218. , width = (lpos.xl - lpos.xi) - this.iwidth
  5219. , drawn = 0
  5220. , visible = 0
  5221. , el;
  5222. if (offset < 0) offset = 0;
  5223. else if (offset >= this.items.length) offset = this.items.length - 1;
  5224. el = this.items[offset];
  5225. if (!el) return;
  5226. this.items.forEach(function(el, i) {
  5227. if (i < self.leftBase) return;
  5228. var lpos = el._getCoords();
  5229. if (!lpos) return;
  5230. // XXX Need this because overflowed elements will still return lpos.
  5231. if (lpos.xl - lpos.xi <= 0) return;
  5232. drawn += (lpos.xl - lpos.xi) + 2;
  5233. // XXX Need this because overflowed elements will still return lpos.
  5234. //drawn += el.getText().length + 2 + 2;
  5235. if (drawn <= width) visible++;
  5236. });
  5237. var diff = offset - (this.leftBase + this.leftOffset);
  5238. if (offset > this.leftBase + this.leftOffset) {
  5239. if (offset > this.leftBase + visible - 1) {
  5240. //this.leftOffset = visible;
  5241. //this.leftBase = offset - visible;
  5242. this.leftOffset = 0;
  5243. this.leftBase = offset;
  5244. } else {
  5245. this.leftOffset += diff;
  5246. }
  5247. } else if (offset < this.leftBase + this.leftOffset) {
  5248. diff = -diff;
  5249. if (offset < this.leftBase) {
  5250. this.leftOffset = 0;
  5251. this.leftBase = offset;
  5252. } else {
  5253. this.leftOffset -= diff;
  5254. }
  5255. }
  5256. //this.leftOffset = Math.max(0, this.leftOffset);
  5257. //this.leftBase = Math.max(0, this.leftBase);
  5258. //this._.debug.setContent(JSON.stringify({
  5259. // leftOffset: this.leftOffset,
  5260. // leftBase: this.leftBase,
  5261. // drawn: drawn,
  5262. // visible: visible,
  5263. // width: width,
  5264. // diff: diff
  5265. //}, null, 2));
  5266. };
  5267. Listbar.prototype.removeItem = function(child) {
  5268. var i = typeof child !== 'number'
  5269. ? this.items.indexOf(child)
  5270. : child;
  5271. if (~i && this.items[i]) {
  5272. child = this.items.splice(i, 1)[0];
  5273. this.ritems.splice(i, 1);
  5274. this.commands.splice(i, 1);
  5275. this.remove(child);
  5276. if (i === this.selected) {
  5277. this.select(i - 1);
  5278. }
  5279. }
  5280. };
  5281. Listbar.prototype.move = function(offset) {
  5282. this.select(this.selected + offset);
  5283. };
  5284. Listbar.prototype.moveLeft = function(offset) {
  5285. this.move(-(offset || 1));
  5286. };
  5287. Listbar.prototype.moveRight = function(offset) {
  5288. this.move(offset || 1);
  5289. };
  5290. Listbar.prototype.selectTab = function(index) {
  5291. var item = this.items[index];
  5292. if (item) {
  5293. //item.press();
  5294. if (item._.cmd.callback) {
  5295. item._.cmd.callback();
  5296. }
  5297. this.select(index);
  5298. this.screen.render();
  5299. }
  5300. };
  5301. /**
  5302. * DirManager - Merge into FileManager?
  5303. */
  5304. function DirManager(options) {
  5305. var self = this;
  5306. if (!(this instanceof Node)) {
  5307. return new DirManager(options);
  5308. }
  5309. options = options || {};
  5310. FileManager.call(this, options);
  5311. this.on('cd', function(dir) {
  5312. if (dir === self.cwd) return;
  5313. self.emit('file', dir);
  5314. });
  5315. }
  5316. DirManager.prototype.__proto__ = FileManager.prototype;
  5317. DirManager.prototype.type = 'dir-manager';
  5318. /**
  5319. * Passbox - Useless
  5320. */
  5321. function Passbox(options) {
  5322. var self = this;
  5323. if (!(this instanceof Node)) {
  5324. return new Passbox(options);
  5325. }
  5326. options = options || {};
  5327. options.censor = true;
  5328. Textbox.call(this, options);
  5329. }
  5330. Passbox.prototype.__proto__ = Textbox.prototype;
  5331. Passbox.prototype.type = 'passbox';
  5332. /**
  5333. * Helpers
  5334. */
  5335. function generateTags(style, text) {
  5336. var open = ''
  5337. , close = '';
  5338. Object.keys(style).forEach(function(key) {
  5339. var val = style[key];
  5340. if (typeof val === 'string') {
  5341. val = val.replace(/^light(?!-)/, 'light-');
  5342. open = '{' + val + '-' + key + '}' + open;
  5343. close += '{/' + val + '-' + key + '}';
  5344. } else {
  5345. if (val) {
  5346. open = '{' + key + '}' + open;
  5347. close += '{/' + key + '}';
  5348. }
  5349. }
  5350. });
  5351. if (text != null) {
  5352. return open + text + close;
  5353. }
  5354. return {
  5355. open: open,
  5356. close: close
  5357. };
  5358. }
  5359. function merge(a, b) {
  5360. Object.keys(b).forEach(function(key) {
  5361. a[key] = b[key];
  5362. });
  5363. return a;
  5364. }
  5365. function asort(obj) {
  5366. return obj.sort(function(a, b) {
  5367. a = a.name.toLowerCase();
  5368. b = b.name.toLowerCase();
  5369. if (a[0] === '.' && b[0] === '.') {
  5370. a = a[1];
  5371. b = b[1];
  5372. } else {
  5373. a = a[0];
  5374. b = b[0];
  5375. }
  5376. return a > b ? 1 : (a < b ? -1 : 0);
  5377. });
  5378. }
  5379. function hsort(obj) {
  5380. return obj.sort(function(a, b) {
  5381. return b.index - a.index;
  5382. });
  5383. }
  5384. var wideChars = new RegExp('(['
  5385. + '\\uff01-\\uffbe'
  5386. + '\\uffc2-\\uffc7'
  5387. + '\\uffca-\\uffcf'
  5388. + '\\uffd2-\\uffd7'
  5389. + '\\uffda-\\uffdc'
  5390. + '\\uffe0-\\uffe6'
  5391. + '\\uffe8-\\uffee'
  5392. + '])', 'g');
  5393. /**
  5394. * Helpers
  5395. */
  5396. var helpers = {};
  5397. // Escape text for tag-enabled elements.
  5398. helpers.escape = function(text) {
  5399. return text.replace(/[{}]/g, function(ch) {
  5400. return ch === '{' ? '{open}' : '{close}';
  5401. });
  5402. };
  5403. /**
  5404. * Expose
  5405. */
  5406. exports.Node = exports.node = Node;
  5407. exports.Screen = exports.screen = Screen;
  5408. exports.Element = exports.element = Element;
  5409. exports.Box = exports.box = Box;
  5410. exports.Text = exports.text = Text;
  5411. exports.Line = exports.line = Line;
  5412. exports.ScrollableBox = exports.scrollablebox = ScrollableBox;
  5413. exports.List = exports.list = List;
  5414. exports.ScrollableText = exports.scrollabletext = ScrollableText;
  5415. exports.Form = exports.form = Form;
  5416. exports.Input = exports.input = Input;
  5417. exports.Textbox = exports.textbox = Textbox;
  5418. exports.Textarea = exports.textarea = Textarea;
  5419. exports.Button = exports.button = Button;
  5420. exports.ProgressBar = exports.progressbar = ProgressBar;
  5421. exports.FileManager = exports.filemanager = FileManager;
  5422. exports.Checkbox = exports.checkbox = Checkbox;
  5423. exports.RadioSet = exports.radioset = RadioSet;
  5424. exports.RadioButton = exports.radiobutton = RadioButton;
  5425. exports.Prompt = exports.prompt = Prompt;
  5426. exports.Question = exports.question = Question;
  5427. exports.Message = exports.message = Message;
  5428. exports.Loading = exports.loading = Loading;
  5429. exports.Listbar = exports.listbar = Listbar;
  5430. exports.DirManager = exports.dirmanager = DirManager;
  5431. exports.Passbox = exports.passbox = Passbox;
  5432. exports.helpers = helpers;