screen-manager.js 3.8 KB

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