diff --git a/example.js b/example.js index 3c00a0d..5544676 100644 --- a/example.js +++ b/example.js @@ -65,3 +65,15 @@ console.log('\n\n' + boxen('This box has a centered title', {title, titleAlignme console.log('\n\n' + boxen('This box has fixed width of 20', {width: 20}) + '\n'); console.log('\n\n' + boxen('This box has fixed width of 50', {width: 50}) + '\n'); + +console.log('\n\n' + boxen('This box has fixed height of 5', {height: 5}) + '\n'); + +console.log('\n\n' + boxen('This box has fixed height of 5', {height: 5, padding: 2}) + '\n'); + +console.log('\n\n' + boxen('This box has fixed height of 5 and width of 15', {height: 8, width: 15}) + '\n'); + +console.log('\n\n' + boxen('This box is in fullscreen !', {fullscreen: true}) + '\n'); + +console.log('\n\n' + boxen('This box is in full-width and half-height !', {fullscreen: (w, h) => [w, h / 2]}) + '\n'); + +console.log('\n\n' + boxen('And this one is in half-width and full-height !', {fullscreen: (w, h) => [w / 2, h]}) + '\n'); diff --git a/index.d.ts b/index.d.ts index cbf7924..ebd495c 100644 --- a/index.d.ts +++ b/index.d.ts @@ -180,7 +180,7 @@ export interface Options { /** Set a fixed width for the box. - **Note*: This disables terminal overflow handling and may cause the box to look broken if the user's terminal is not wide enough. + __Note__: This disables terminal overflow handling and may cause the box to look broken if the user's terminal is not wide enough. @example ``` @@ -193,6 +193,41 @@ export interface Options { ``` */ readonly width?: number; + + /** + Set a fixed height for the box. + + __Note__: This option will crop overflowing content. + + @example + ``` + import boxen from 'boxen'; + + console.log(boxen('foo bar', {height: 5})); + // ┌───────┐ + // │foo bar│ + // │ │ + // │ │ + // └───────┘ + ``` + */ + readonly height?: number; + + /** + __boolean__: Wether or not to fit all available space within the terminal. + + __function__: Pass a callback function to control box dimensions. + + @example + ``` + import boxen from 'boxen'; + + console.log(boxen('foo bar', { + fullscreen: (width, height) => [width, height - 1]; + })); + ``` + */ + readonly fullscreen?: boolean | ((width: number, height: number) => [width: number, height: number]); } /** diff --git a/index.js b/index.js index ff98256..64cdd79 100644 --- a/index.js +++ b/index.js @@ -116,24 +116,24 @@ const makeTitle = (text, horizontal, alignement) => { return title; }; -const makeContentText = (text, padding, columns, align) => { - text = ansiAlign(text, {align}); +const makeContentText = (text, {padding, width, textAlignment, height}) => { + text = ansiAlign(text, {align: textAlignment}); let lines = text.split(NEWLINE); const textWidth = widestLine(text); - const max = columns - padding.left - padding.right; + const max = width - padding.left - padding.right; if (textWidth > max) { const newLines = []; for (const line of lines) { const createdLines = wrapAnsi(line, max, {hard: true}); - const alignedLines = ansiAlign(createdLines, {align}); + const alignedLines = ansiAlign(createdLines, {align: textAlignment}); const alignedLinesArray = alignedLines.split('\n'); const longestLength = Math.max(...alignedLinesArray.map(s => stringWidth(s))); for (const alignedLine of alignedLinesArray) { let paddedLine; - switch (align) { + switch (textAlignment) { case 'center': paddedLine = PAD.repeat((max - longestLength) / 2) + alignedLine; break; @@ -152,9 +152,9 @@ const makeContentText = (text, padding, columns, align) => { lines = newLines; } - if (align === 'center' && textWidth < max) { + if (textAlignment === 'center' && textWidth < max) { lines = lines.map(line => PAD.repeat((max - textWidth) / 2) + line); - } else if (align === 'right' && textWidth < max) { + } else if (textAlignment === 'right' && textWidth < max) { lines = lines.map(line => PAD.repeat(max - textWidth) + line); } @@ -164,14 +164,14 @@ const makeContentText = (text, padding, columns, align) => { lines = lines.map(line => paddingLeft + line + paddingRight); lines = lines.map(line => { - if (columns - stringWidth(line) > 0) { - switch (align) { + if (width - stringWidth(line) > 0) { + switch (textAlignment) { case 'center': - return line + PAD.repeat(columns - stringWidth(line)); + return line + PAD.repeat(width - stringWidth(line)); case 'right': - return line + PAD.repeat(columns - stringWidth(line)); + return line + PAD.repeat(width - stringWidth(line)); default: - return line + PAD.repeat(columns - stringWidth(line)); + return line + PAD.repeat(width - stringWidth(line)); } } @@ -179,11 +179,17 @@ const makeContentText = (text, padding, columns, align) => { }); if (padding.top > 0) { - lines = [...Array.from({length: padding.top}).fill(PAD.repeat(columns)), ...lines]; + lines = [...Array.from({length: padding.top}).fill(PAD.repeat(width)), ...lines]; } if (padding.bottom > 0) { - lines = [...lines, ...Array.from({length: padding.bottom}).fill(PAD.repeat(columns))]; + lines = [...lines, ...Array.from({length: padding.bottom}).fill(PAD.repeat(width))]; + } + + if (height && lines.length > height) { + lines = lines.slice(0, height); + } else if (height && lines.length < height) { + lines = [...lines, ...Array.from({length: height - lines.length}).fill(PAD.repeat(width))]; } return lines.join(NEWLINE); @@ -221,16 +227,43 @@ const boxContent = (content, contentWidth, options) => { return top + LINE_SEPARATOR + middle + LINE_SEPARATOR + bottom; }; -const determineDimensions = (text, options) => { - const widthOverride = options.width !== undefined; - const columns = terminalColumns(); - const maxWidth = columns - options.margin.left - options.margin.right - BORDERS_WIDTH; +const sanitizeOptions = options => { + // If fullscreen is enabled, max-out unspecified width/height + if (options.fullscreen && process && process.stdout) { + let newDimensions = [process.stdout.columns, process.stdout.rows]; + + if (typeof options.fullscreen === 'function') { + newDimensions = options.fullscreen(...newDimensions); + } + + if (!options.width) { + options.width = newDimensions[0]; + } + + if (!options.height) { + options.height = newDimensions[1]; + } + } // If width is provided, make sure it's not below 1 if (options.width) { options.width = Math.max(1, options.width - BORDERS_WIDTH); } + // If height is provided, make sure it's not below 1 + if (options.height) { + options.height = Math.max(1, options.height - BORDERS_WIDTH); + } + + return options; +}; + +const determineDimensions = (text, options) => { + options = sanitizeOptions(options); + const widthOverride = options.width !== undefined; + const columns = terminalColumns(); + const maxWidth = columns - options.margin.left - options.margin.right - BORDERS_WIDTH; + const widest = widestLine(wrapAnsi(text, columns - BORDERS_WIDTH, {hard: true, trim: false})) + options.padding.left + options.padding.right; // If title and width are provided, title adheres to fixed width @@ -278,6 +311,11 @@ const determineDimensions = (text, options) => { options.padding.right = 0; } + if (options.height && options.height - (options.padding.top + options.padding.bottom) <= 0) { + options.padding.top = 0; + options.padding.bottom = 0; + } + return options; }; @@ -315,7 +353,7 @@ export default function boxen(text, options) { options = determineDimensions(text, options); - text = makeContentText(text, options.padding, options.width, options.textAlignment); + text = makeContentText(text, options); return boxContent(text, options.width, options); } diff --git a/index.test-d.ts b/index.test-d.ts index 8efe731..00f5330 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -36,3 +36,5 @@ expectType(boxen('unicorns', {backgroundColor: 'green'})); expectType(boxen('unicorns', {backgroundColor: '#ff0000'})); expectType(boxen('unicorns', {textAlignment: 'right'})); expectType(boxen('unicorns', {width: 20})); +expectType(boxen('unicorns', {height: 5})); +expectType(boxen('unicorns', {fullscreen: true})); diff --git a/readme.md b/readme.md index eae061e..e1ab14b 100644 --- a/readme.md +++ b/readme.md @@ -200,6 +200,50 @@ Set a fixed width for the box. *Note:* This disables terminal overflow handling and may cause the box to look broken if the user's terminal is not wide enough. +```js +import boxen from 'boxen'; + +console.log(boxen('foo bar', {width: 15})); +// ┌─────────────┐ +// │foo bar │ +// └─────────────┘ +``` + +##### height + +Type: `number` + +Set a fixed height for the box. + +*Note:* This option will crop overflowing content. + +```js +import boxen from 'boxen'; + +console.log(boxen('foo bar', {height: 5})); +// ┌───────┐ +// │foo bar│ +// │ │ +// │ │ +// └───────┘ +``` + +##### fullscreen + +Type: `boolean | (width: number, height: number) => [width: number, height: number]` + +Wether or not to fit all available space within the terminal. + +Pass a callback function to control box dimensions: + +```js +import boxen from 'boxen'; + +console.log(boxen('foo bar', { + fullscreen: (width, height) => [width, height - 1]; +})); +``` + ##### padding Type: `number | object`\ diff --git a/tests/fullscreen-option.js b/tests/fullscreen-option.js new file mode 100644 index 0000000..ec84ae2 --- /dev/null +++ b/tests/fullscreen-option.js @@ -0,0 +1,36 @@ +import test from 'ava'; +import boxen from '../index.js'; + +test('fullscreen option', t => { + const box = boxen('foo', { + fullscreen: true, + }); + + t.snapshot(box); +}); + +test('fullscreen option + width', t => { + const box = boxen('foo', { + fullscreen: true, + width: 10, + }); + + t.snapshot(box); +}); + +test('fullscreen option + height', t => { + const box = boxen('foo', { + fullscreen: true, + height: 10, + }); + + t.snapshot(box); +}); + +test('fullscreen option with callback', t => { + const box = boxen('foo', { + fullscreen: (width, height) => [width - 2, height - 2], + }); + + t.snapshot(box); +}); diff --git a/tests/height-option.js b/tests/height-option.js new file mode 100644 index 0000000..0da3a79 --- /dev/null +++ b/tests/height-option.js @@ -0,0 +1,51 @@ +import test from 'ava'; +import boxen from '../index.js'; + +test('height option works', t => { + // Creates a tall box with empty rows + t.snapshot( + boxen('foo', { + height: 5, + }), + ); + + // Creates a 1 line box, cropping the other lines + t.snapshot( + boxen('foo bar\nfoo bar', { + height: 3, + }), + ); +}); + +test('height option with padding + margin', t => { + // Creates a wide box for little text + const box = boxen('foo', { + height: 20, + margin: 2, + padding: 1, + }); + + t.snapshot(box); +}); + +test('height option with width', t => { + // Creates a wide box for little text + const box = boxen('foo', { + height: 5, + width: 20, + }); + + t.snapshot(box); +}); + +test('height option with width + padding + margin', t => { + // Creates a wide box for little text + const box = boxen('foo', { + height: 5, + width: 20, + margin: 2, + padding: 1, + }); + + t.snapshot(box); +}); diff --git a/tests/snapshots/tests/fullscreen-option.js.md b/tests/snapshots/tests/fullscreen-option.js.md new file mode 100644 index 0000000..08a1e1e --- /dev/null +++ b/tests/snapshots/tests/fullscreen-option.js.md @@ -0,0 +1,44 @@ +# Snapshot report for `tests/fullscreen-option.js` + +The actual snapshot is saved in `fullscreen-option.js.snap`. + +Generated by [AVA](https://avajs.dev). + +## fullscreen option + +> Snapshot 1 + + `┌───┐␊ + │foo│␊ + └───┘` + +## fullscreen option + width + +> Snapshot 1 + + `┌────────┐␊ + │foo │␊ + └────────┘` + +## fullscreen option + height + +> Snapshot 1 + + `┌───┐␊ + │foo│␊ + │ │␊ + │ │␊ + │ │␊ + │ │␊ + │ │␊ + │ │␊ + │ │␊ + └───┘` + +## fullscreen option with callback + +> Snapshot 1 + + `┌───┐␊ + │foo│␊ + └───┘` diff --git a/tests/snapshots/tests/fullscreen-option.js.snap b/tests/snapshots/tests/fullscreen-option.js.snap new file mode 100644 index 0000000..85d0f27 Binary files /dev/null and b/tests/snapshots/tests/fullscreen-option.js.snap differ diff --git a/tests/snapshots/tests/height-option.js.md b/tests/snapshots/tests/height-option.js.md new file mode 100644 index 0000000..7215674 --- /dev/null +++ b/tests/snapshots/tests/height-option.js.md @@ -0,0 +1,74 @@ +# Snapshot report for `tests/height-option.js` + +The actual snapshot is saved in `height-option.js.snap`. + +Generated by [AVA](https://avajs.dev). + +## height option works + +> Snapshot 1 + + `┌───┐␊ + │foo│␊ + │ │␊ + │ │␊ + └───┘` + +> Snapshot 2 + + `┌───────┐␊ + │foo bar│␊ + └───────┘` + +## height option with padding + margin + +> Snapshot 1 + + `␊ + ␊ + ┌─────────┐␊ + │ │␊ + │ foo │␊ + │ │␊ + │ │␊ + │ │␊ + │ │␊ + │ │␊ + │ │␊ + │ │␊ + │ │␊ + │ │␊ + │ │␊ + │ │␊ + │ │␊ + │ │␊ + │ │␊ + │ │␊ + │ │␊ + └─────────┘␊ + ␊ + ` + +## height option with width + +> Snapshot 1 + + `┌──────────────────┐␊ + │foo │␊ + │ │␊ + │ │␊ + └──────────────────┘` + +## height option with width + padding + margin + +> Snapshot 1 + + `␊ + ␊ + ┌──────────────────┐␊ + │ │␊ + │ foo │␊ + │ │␊ + └──────────────────┘␊ + ␊ + ` diff --git a/tests/snapshots/tests/height-option.js.snap b/tests/snapshots/tests/height-option.js.snap new file mode 100644 index 0000000..fa01fe2 Binary files /dev/null and b/tests/snapshots/tests/height-option.js.snap differ