index.js 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. 'use strict';
  2. const align = {
  3. right: alignRight,
  4. center: alignCenter
  5. };
  6. const top = 0;
  7. const right = 1;
  8. const bottom = 2;
  9. const left = 3;
  10. export class UI {
  11. constructor(opts) {
  12. var _a;
  13. this.width = opts.width;
  14. this.wrap = (_a = opts.wrap) !== null && _a !== void 0 ? _a : true;
  15. this.rows = [];
  16. }
  17. span(...args) {
  18. const cols = this.div(...args);
  19. cols.span = true;
  20. }
  21. resetOutput() {
  22. this.rows = [];
  23. }
  24. div(...args) {
  25. if (args.length === 0) {
  26. this.div('');
  27. }
  28. if (this.wrap && this.shouldApplyLayoutDSL(...args) && typeof args[0] === 'string') {
  29. return this.applyLayoutDSL(args[0]);
  30. }
  31. const cols = args.map(arg => {
  32. if (typeof arg === 'string') {
  33. return this.colFromString(arg);
  34. }
  35. return arg;
  36. });
  37. this.rows.push(cols);
  38. return cols;
  39. }
  40. shouldApplyLayoutDSL(...args) {
  41. return args.length === 1 && typeof args[0] === 'string' &&
  42. /[\t\n]/.test(args[0]);
  43. }
  44. applyLayoutDSL(str) {
  45. const rows = str.split('\n').map(row => row.split('\t'));
  46. let leftColumnWidth = 0;
  47. // simple heuristic for layout, make sure the
  48. // second column lines up along the left-hand.
  49. // don't allow the first column to take up more
  50. // than 50% of the screen.
  51. rows.forEach(columns => {
  52. if (columns.length > 1 && mixin.stringWidth(columns[0]) > leftColumnWidth) {
  53. leftColumnWidth = Math.min(Math.floor(this.width * 0.5), mixin.stringWidth(columns[0]));
  54. }
  55. });
  56. // generate a table:
  57. // replacing ' ' with padding calculations.
  58. // using the algorithmically generated width.
  59. rows.forEach(columns => {
  60. this.div(...columns.map((r, i) => {
  61. return {
  62. text: r.trim(),
  63. padding: this.measurePadding(r),
  64. width: (i === 0 && columns.length > 1) ? leftColumnWidth : undefined
  65. };
  66. }));
  67. });
  68. return this.rows[this.rows.length - 1];
  69. }
  70. colFromString(text) {
  71. return {
  72. text,
  73. padding: this.measurePadding(text)
  74. };
  75. }
  76. measurePadding(str) {
  77. // measure padding without ansi escape codes
  78. const noAnsi = mixin.stripAnsi(str);
  79. return [0, noAnsi.match(/\s*$/)[0].length, 0, noAnsi.match(/^\s*/)[0].length];
  80. }
  81. toString() {
  82. const lines = [];
  83. this.rows.forEach(row => {
  84. this.rowToString(row, lines);
  85. });
  86. // don't display any lines with the
  87. // hidden flag set.
  88. return lines
  89. .filter(line => !line.hidden)
  90. .map(line => line.text)
  91. .join('\n');
  92. }
  93. rowToString(row, lines) {
  94. this.rasterize(row).forEach((rrow, r) => {
  95. let str = '';
  96. rrow.forEach((col, c) => {
  97. const { width } = row[c]; // the width with padding.
  98. const wrapWidth = this.negatePadding(row[c]); // the width without padding.
  99. let ts = col; // temporary string used during alignment/padding.
  100. if (wrapWidth > mixin.stringWidth(col)) {
  101. ts += ' '.repeat(wrapWidth - mixin.stringWidth(col));
  102. }
  103. // align the string within its column.
  104. if (row[c].align && row[c].align !== 'left' && this.wrap) {
  105. const fn = align[row[c].align];
  106. ts = fn(ts, wrapWidth);
  107. if (mixin.stringWidth(ts) < wrapWidth) {
  108. ts += ' '.repeat((width || 0) - mixin.stringWidth(ts) - 1);
  109. }
  110. }
  111. // apply border and padding to string.
  112. const padding = row[c].padding || [0, 0, 0, 0];
  113. if (padding[left]) {
  114. str += ' '.repeat(padding[left]);
  115. }
  116. str += addBorder(row[c], ts, '| ');
  117. str += ts;
  118. str += addBorder(row[c], ts, ' |');
  119. if (padding[right]) {
  120. str += ' '.repeat(padding[right]);
  121. }
  122. // if prior row is span, try to render the
  123. // current row on the prior line.
  124. if (r === 0 && lines.length > 0) {
  125. str = this.renderInline(str, lines[lines.length - 1]);
  126. }
  127. });
  128. // remove trailing whitespace.
  129. lines.push({
  130. text: str.replace(/ +$/, ''),
  131. span: row.span
  132. });
  133. });
  134. return lines;
  135. }
  136. // if the full 'source' can render in
  137. // the target line, do so.
  138. renderInline(source, previousLine) {
  139. const match = source.match(/^ */);
  140. const leadingWhitespace = match ? match[0].length : 0;
  141. const target = previousLine.text;
  142. const targetTextWidth = mixin.stringWidth(target.trimRight());
  143. if (!previousLine.span) {
  144. return source;
  145. }
  146. // if we're not applying wrapping logic,
  147. // just always append to the span.
  148. if (!this.wrap) {
  149. previousLine.hidden = true;
  150. return target + source;
  151. }
  152. if (leadingWhitespace < targetTextWidth) {
  153. return source;
  154. }
  155. previousLine.hidden = true;
  156. return target.trimRight() + ' '.repeat(leadingWhitespace - targetTextWidth) + source.trimLeft();
  157. }
  158. rasterize(row) {
  159. const rrows = [];
  160. const widths = this.columnWidths(row);
  161. let wrapped;
  162. // word wrap all columns, and create
  163. // a data-structure that is easy to rasterize.
  164. row.forEach((col, c) => {
  165. // leave room for left and right padding.
  166. col.width = widths[c];
  167. if (this.wrap) {
  168. wrapped = mixin.wrap(col.text, this.negatePadding(col), { hard: true }).split('\n');
  169. }
  170. else {
  171. wrapped = col.text.split('\n');
  172. }
  173. if (col.border) {
  174. wrapped.unshift('.' + '-'.repeat(this.negatePadding(col) + 2) + '.');
  175. wrapped.push("'" + '-'.repeat(this.negatePadding(col) + 2) + "'");
  176. }
  177. // add top and bottom padding.
  178. if (col.padding) {
  179. wrapped.unshift(...new Array(col.padding[top] || 0).fill(''));
  180. wrapped.push(...new Array(col.padding[bottom] || 0).fill(''));
  181. }
  182. wrapped.forEach((str, r) => {
  183. if (!rrows[r]) {
  184. rrows.push([]);
  185. }
  186. const rrow = rrows[r];
  187. for (let i = 0; i < c; i++) {
  188. if (rrow[i] === undefined) {
  189. rrow.push('');
  190. }
  191. }
  192. rrow.push(str);
  193. });
  194. });
  195. return rrows;
  196. }
  197. negatePadding(col) {
  198. let wrapWidth = col.width || 0;
  199. if (col.padding) {
  200. wrapWidth -= (col.padding[left] || 0) + (col.padding[right] || 0);
  201. }
  202. if (col.border) {
  203. wrapWidth -= 4;
  204. }
  205. return wrapWidth;
  206. }
  207. columnWidths(row) {
  208. if (!this.wrap) {
  209. return row.map(col => {
  210. return col.width || mixin.stringWidth(col.text);
  211. });
  212. }
  213. let unset = row.length;
  214. let remainingWidth = this.width;
  215. // column widths can be set in config.
  216. const widths = row.map(col => {
  217. if (col.width) {
  218. unset--;
  219. remainingWidth -= col.width;
  220. return col.width;
  221. }
  222. return undefined;
  223. });
  224. // any unset widths should be calculated.
  225. const unsetWidth = unset ? Math.floor(remainingWidth / unset) : 0;
  226. return widths.map((w, i) => {
  227. if (w === undefined) {
  228. return Math.max(unsetWidth, _minWidth(row[i]));
  229. }
  230. return w;
  231. });
  232. }
  233. }
  234. function addBorder(col, ts, style) {
  235. if (col.border) {
  236. if (/[.']-+[.']/.test(ts)) {
  237. return '';
  238. }
  239. if (ts.trim().length !== 0) {
  240. return style;
  241. }
  242. return ' ';
  243. }
  244. return '';
  245. }
  246. // calculates the minimum width of
  247. // a column, based on padding preferences.
  248. function _minWidth(col) {
  249. const padding = col.padding || [];
  250. const minWidth = 1 + (padding[left] || 0) + (padding[right] || 0);
  251. if (col.border) {
  252. return minWidth + 4;
  253. }
  254. return minWidth;
  255. }
  256. function getWindowWidth() {
  257. /* istanbul ignore next: depends on terminal */
  258. if (typeof process === 'object' && process.stdout && process.stdout.columns) {
  259. return process.stdout.columns;
  260. }
  261. return 80;
  262. }
  263. function alignRight(str, width) {
  264. str = str.trim();
  265. const strWidth = mixin.stringWidth(str);
  266. if (strWidth < width) {
  267. return ' '.repeat(width - strWidth) + str;
  268. }
  269. return str;
  270. }
  271. function alignCenter(str, width) {
  272. str = str.trim();
  273. const strWidth = mixin.stringWidth(str);
  274. /* istanbul ignore next */
  275. if (strWidth >= width) {
  276. return str;
  277. }
  278. return ' '.repeat((width - strWidth) >> 1) + str;
  279. }
  280. let mixin;
  281. export function cliui(opts, _mixin) {
  282. mixin = _mixin;
  283. return new UI({
  284. width: (opts === null || opts === void 0 ? void 0 : opts.width) || getWindowWidth(),
  285. wrap: opts === null || opts === void 0 ? void 0 : opts.wrap
  286. });
  287. }