Skip to content

Commit 9555d3f

Browse files
Add width option (#70)
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
1 parent 5f88b63 commit 9555d3f

11 files changed

Lines changed: 227 additions & 31 deletions

example.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,7 @@ const title = 'Beautiful title';
6161
console.log('\n\n' + boxen('This box has a nice title', {title}) + '\n');
6262

6363
console.log('\n\n' + boxen('This box has a centered title', {title, titleAlignment: 'center'}) + '\n');
64+
65+
console.log('\n\n' + boxen('This box has fixed width of 20', {width: 20}) + '\n');
66+
67+
console.log('\n\n' + boxen('This box has fixed width of 50', {width: 50}) + '\n');

index.d.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,23 @@ export interface Options {
176176
```
177177
*/
178178
readonly titleAlignment?: 'left' | 'right' | 'center';
179+
180+
/**
181+
Set a fixed width for the box.
182+
183+
**Note*: This disables terminal overflow handling and may cause the box to look broken if the user's terminal is not wide enough.
184+
185+
@example
186+
```
187+
import boxen from 'boxen';
188+
189+
console.log(boxen('foo bar', {width: 15}));
190+
// ┌─────────────┐
191+
// │foo bar │
192+
// └─────────────┘
193+
```
194+
*/
195+
readonly width?: number;
179196
}
180197

181198
/**

index.js

Lines changed: 63 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,66 @@ const boxContent = (content, contentWidth, options) => {
221221
return top + LINE_SEPARATOR + middle + LINE_SEPARATOR + bottom;
222222
};
223223

224+
const determineDimensions = (text, options) => {
225+
const widthOverride = options.width !== undefined;
226+
const columns = terminalColumns();
227+
const maxWidth = columns - options.margin.left - options.margin.right - BORDERS_WIDTH;
228+
229+
// If width is provided, make sure it's not below 1
230+
if (options.width) {
231+
options.width = Math.max(1, options.width - BORDERS_WIDTH);
232+
}
233+
234+
const widest = widestLine(wrapAnsi(text, columns - BORDERS_WIDTH, {hard: true, trim: false})) + options.padding.left + options.padding.right;
235+
236+
// If title and width are provided, title adheres to fixed width
237+
if (options.title && widthOverride) {
238+
options.title = options.title.slice(0, Math.max(0, options.width - 2));
239+
if (options.title) {
240+
options.title = ` ${options.title} `;
241+
}
242+
} else if (options.title) {
243+
options.title = options.title.slice(0, Math.max(0, maxWidth - 2));
244+
245+
// Recheck if title isn't empty now
246+
if (options.title) {
247+
options.title = ` ${options.title} `;
248+
// If the title is larger than content, box adheres to title width
249+
if (stringWidth(options.title) > widest) {
250+
options.width = stringWidth(options.title);
251+
}
252+
}
253+
}
254+
255+
// If fixed width is provided, use it or content width as reference
256+
options.width = options.width ? options.width : widest;
257+
258+
if (!widthOverride) {
259+
if ((options.margin.left && options.margin.right) && options.width > maxWidth) {
260+
// Let's assume we have margins: left = 3, right = 5, in total = 8
261+
const spaceForMargins = columns - options.width - BORDERS_WIDTH;
262+
// Let's assume we have space = 4
263+
const multiplier = spaceForMargins / (options.margin.left + options.margin.right);
264+
// Here: multiplier = 4/8 = 0.5
265+
options.margin.left = Math.max(0, Math.floor(options.margin.left * multiplier));
266+
options.margin.right = Math.max(0, Math.floor(options.margin.right * multiplier));
267+
// Left: 3 * 0.5 = 1.5 -> 1
268+
// Right: 6 * 0.5 = 3
269+
}
270+
271+
// Re-cap width considering the margins after shrinking
272+
options.width = Math.min(options.width, columns - BORDERS_WIDTH - options.margin.left - options.margin.right);
273+
}
274+
275+
// Prevent padding overflow
276+
if (options.width - (options.padding.left + options.padding.right) <= 0) {
277+
options.padding.left = 0;
278+
options.padding.right = 0;
279+
}
280+
281+
return options;
282+
};
283+
224284
const isHex = color => color.match(/^#(?:[0-f]{3}){1,2}$/i);
225285
const isColorValid = color => typeof color === 'string' && ((chalk[color]) || isHex(color));
226286
const getColorFn = color => isHex(color) ? chalk.hex(color) : chalk[color];
@@ -253,39 +313,11 @@ export default function boxen(text, options) {
253313
options.padding = getObject(options.padding);
254314
options.margin = getObject(options.margin);
255315

256-
const columns = terminalColumns();
257-
258-
let contentWidth = widestLine(wrapAnsi(text, columns - BORDERS_WIDTH, {hard: true, trim: false})) + options.padding.left + options.padding.right;
259-
260-
// This prevents the title bar to exceed the console's width
261-
options.title = options.title && options.title.slice(0, columns - 4 - options.margin.left - options.margin.right);
262-
263-
if (options.title) {
264-
options.title = ` ${options.title} `;
265-
// Make the box larger to fit a larger title
266-
if (stringWidth(options.title) > contentWidth) {
267-
contentWidth = stringWidth(options.title);
268-
}
269-
}
270-
271-
if ((options.margin.left && options.margin.right) && contentWidth + BORDERS_WIDTH + options.margin.left + options.margin.right > columns) {
272-
// Let's assume we have margins: left = 3, right = 5, in total = 8
273-
const spaceForMargins = columns - contentWidth - BORDERS_WIDTH;
274-
// Let's assume we have space = 4
275-
const multiplier = spaceForMargins / (options.margin.left + options.margin.right);
276-
// Here: multiplier = 4/8 = 0.5
277-
options.margin.left = Math.max(0, Math.floor(options.margin.left * multiplier));
278-
options.margin.right = Math.max(0, Math.floor(options.margin.right * multiplier));
279-
// Left: 3 * 0.5 = 1.5 -> 1
280-
// Right: 6 * 0.5 = 3
281-
}
282-
283-
// Prevent content from exceeding the console's width
284-
contentWidth = Math.min(contentWidth, columns - BORDERS_WIDTH - options.margin.left - options.margin.right);
316+
options = determineDimensions(text, options);
285317

286-
text = makeContentText(text, options.padding, contentWidth, options.textAlignment);
318+
text = makeContentText(text, options.padding, options.width, options.textAlignment);
287319

288-
return boxContent(text, contentWidth, options);
320+
return boxContent(text, options.width, options);
289321
}
290322

291323
export const _borderStyles = cliBoxes;

index.test-d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,4 @@ expectType<string>(boxen('unicorns', {float: 'center'}));
3535
expectType<string>(boxen('unicorns', {backgroundColor: 'green'}));
3636
expectType<string>(boxen('unicorns', {backgroundColor: '#ff0000'}));
3737
expectType<string>(boxen('unicorns', {textAlignment: 'right'}));
38+
expectType<string>(boxen('unicorns', {width: 20}));

readme.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,14 @@ Values:
192192
*/
193193
```
194194

195+
##### width
196+
197+
Type: `number`
198+
199+
Set a fixed width for the box.
200+
201+
*Note:* This disables terminal overflow handling and may cause the box to look broken if the user's terminal is not wide enough.
202+
195203
##### padding
196204

197205
Type: `number | object`\

tests/snapshots/tests/title-option.js.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,25 @@ Generated by [AVA](https://avajs.dev).
4343
`┌ very long title ┐␊
4444
│foo │␊
4545
└─────────────────┘`
46+
47+
## title + width option
48+
49+
> Snapshot 1
50+
51+
`┌─┐␊
52+
│f│␊
53+
│o│␊
54+
│o│␊
55+
└─┘`
56+
57+
> Snapshot 2
58+
59+
`┌ v ┐␊
60+
│foo│␊
61+
└───┘`
62+
63+
> Snapshot 3
64+
65+
`┌ very long title ─┐␊
66+
│foo │␊
67+
└──────────────────┘`
66 Bytes
Binary file not shown.
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Snapshot report for `tests/width-option.js`
2+
3+
The actual snapshot is saved in `width-option.js.snap`.
4+
5+
Generated by [AVA](https://avajs.dev).
6+
7+
## width option works
8+
9+
> Snapshot 1
10+
11+
`┌──────────────────┐␊
12+
│foo │␊
13+
└──────────────────┘`
14+
15+
> Snapshot 2
16+
17+
`┌────────┐␊
18+
│foo bar │␊
19+
│foo bar │␊
20+
└────────┘`
21+
22+
## width option with padding + margin
23+
24+
> Snapshot 1
25+
26+
`␊
27+
28+
┌──────────────────┐␊
29+
│ │␊
30+
│ foo │␊
31+
│ │␊
32+
└──────────────────┘␊
33+
34+
`
35+
36+
## width option with big padding
37+
38+
> Snapshot 1
39+
40+
`┌────┐␊
41+
│ │␊
42+
│ │␊
43+
│ │␊
44+
│foo │␊
45+
│ │␊
46+
│ │␊
47+
│ │␊
48+
└────┘`
247 Bytes
Binary file not shown.

tests/title-option.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,28 @@ test('long title expands box', t => {
4343

4444
t.snapshot(box);
4545
});
46+
47+
test('title + width option', t => {
48+
// Not enough space, no title
49+
t.snapshot(
50+
boxen('foo', {
51+
title: 'very long title',
52+
width: 3,
53+
}),
54+
);
55+
56+
// Space for only one character
57+
t.snapshot(
58+
boxen('foo', {
59+
title: 'very long title',
60+
width: 5,
61+
}),
62+
);
63+
64+
t.snapshot(
65+
boxen('foo', {
66+
title: 'very long title',
67+
width: 20,
68+
}),
69+
);
70+
});

0 commit comments

Comments
 (0)