index.js 9.9 KB

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