screen-manager.js 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  1. 'use strict';
  2. var _ = require('lodash');
  3. var util = require('./readline');
  4. var cliWidth = require('cli-width');
  5. var stripAnsi = require('strip-ansi');
  6. var stringWidth = require('string-width');
  7. function height(content) {
  8. return content.split('\n').length;
  9. }
  10. function lastLine(content) {
  11. return _.last(content.split('\n'));
  12. }
  13. var ScreenManager = module.exports = function (rl) {
  14. // These variables are keeping information to allow correct prompt re-rendering
  15. this.height = 0;
  16. this.extraLinesUnderPrompt = 0;
  17. this.rl = rl;
  18. };
  19. ScreenManager.prototype.render = function (content, bottomContent) {
  20. this.rl.output.unmute();
  21. this.clean(this.extraLinesUnderPrompt);
  22. /**
  23. * Write message to screen and setPrompt to control backspace
  24. */
  25. var promptLine = lastLine(content);
  26. var rawPromptLine = stripAnsi(promptLine);
  27. // Remove the rl.line from our prompt. We can't rely on the content of
  28. // rl.line (mainly because of the password prompt), so just rely on it's
  29. // length.
  30. var prompt = rawPromptLine;
  31. if (this.rl.line.length) {
  32. prompt = prompt.slice(0, -this.rl.line.length);
  33. }
  34. this.rl.setPrompt(prompt);
  35. // setPrompt will change cursor position, now we can get correct value
  36. var cursorPos = this.rl._getCursorPos();
  37. var width = this.normalizedCliWidth();
  38. content = forceLineReturn(content, width);
  39. if (bottomContent) {
  40. bottomContent = forceLineReturn(bottomContent, width);
  41. }
  42. // Manually insert an extra line if we're at the end of the line.
  43. // This prevent the cursor from appearing at the beginning of the
  44. // current line.
  45. if (rawPromptLine.length % width === 0) {
  46. content += '\n';
  47. }
  48. var fullContent = content + (bottomContent ? '\n' + bottomContent : '');
  49. this.rl.output.write(fullContent);
  50. /**
  51. * Re-adjust the cursor at the correct position.
  52. */
  53. // We need to consider parts of the prompt under the cursor as part of the bottom
  54. // content in order to correctly cleanup and re-render.
  55. var promptLineUpDiff = Math.floor(rawPromptLine.length / width) - cursorPos.rows;
  56. var bottomContentHeight = promptLineUpDiff + (bottomContent ? height(bottomContent) : 0);
  57. if (bottomContentHeight > 0) {
  58. util.up(this.rl, bottomContentHeight);
  59. }
  60. // Reset cursor at the beginning of the line
  61. util.left(this.rl, stringWidth(lastLine(fullContent)));
  62. // Adjust cursor on the right
  63. util.right(this.rl, cursorPos.cols);
  64. /**
  65. * Set up state for next re-rendering
  66. */
  67. this.extraLinesUnderPrompt = bottomContentHeight;
  68. this.height = height(fullContent);
  69. this.rl.output.mute();
  70. };
  71. ScreenManager.prototype.clean = function (extraLines) {
  72. if (extraLines > 0) {
  73. util.down(this.rl, extraLines);
  74. }
  75. util.clearLine(this.rl, this.height);
  76. };
  77. ScreenManager.prototype.done = function () {
  78. this.rl.setPrompt('');
  79. this.rl.output.unmute();
  80. this.rl.output.write('\n');
  81. };
  82. ScreenManager.prototype.releaseCursor = function () {
  83. if (this.extraLinesUnderPrompt > 0) {
  84. util.down(this.rl, this.extraLinesUnderPrompt);
  85. }
  86. };
  87. ScreenManager.prototype.normalizedCliWidth = function () {
  88. var width = cliWidth({
  89. defaultWidth: 80,
  90. output: this.rl.output
  91. });
  92. if (process.platform === 'win32') {
  93. return width - 1;
  94. }
  95. return width;
  96. };
  97. function breakLines(lines, width) {
  98. // Break lines who're longuer than the cli width so we can normalize the natural line
  99. // returns behavior accross terminals.
  100. var regex = new RegExp(
  101. '(?:(?:\\033[[0-9;]*m)*.?){1,' + width + '}',
  102. 'g'
  103. );
  104. return lines.map(function (line) {
  105. var chunk = line.match(regex);
  106. // last match is always empty
  107. chunk.pop();
  108. return chunk || '';
  109. });
  110. }
  111. function forceLineReturn(content, width) {
  112. return _.flatten(breakLines(content.split('\n'), width)).join('\n');
  113. }