Skip to content

fix: calculate box width using display width#416

Open
lawrence3699 wants to merge 1 commit into
unjs:mainfrom
lawrence3699:fix/box-display-width
Open

fix: calculate box width using display width#416
lawrence3699 wants to merge 1 commit into
unjs:mainfrom
lawrence3699:fix/box-display-width

Conversation

@lawrence3699

@lawrence3699 lawrence3699 commented Apr 22, 2026

Copy link
Copy Markdown

resolves #402

box() currently uses .length when it calculates content/title width and the trailing padding for each line. That makes the content row narrower than the borders for wide glyphs such as 🚀️ and 漢字.

This switches those calculations to display width via string-width and adds a regression test that checks all rendered box lines keep the same visible width for wide-character content.

Validation:

  • pnpm test
  • pnpm build

Summary by CodeRabbit

  • Bug Fixes

    • Fixed box alignment for text containing special characters, formatting codes, and CJK/emoji content.
  • Tests

    • Added test suite verifying consistent line alignment in box rendering output.

Copilot AI review requested due to automatic review settings April 22, 2026 01:46
@coderabbitai

coderabbitai Bot commented Apr 22, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

The changes fix emoji misalignment in the box utility by introducing a stringWidth helper function that properly calculates display width before layout calculations. A test suite validates the fix against emojis and CJK characters.

Changes

Cohort / File(s) Summary
Box Utility Fix
src/utils/box.ts
Added stringWidth helper to correctly compute display width accounting for ANSI codes. Updated box dimension calculations and title centering logic to use display width instead of character .length for proper alignment with multi-byte characters and emojis.
Test Suite
test/box.test.ts
New Vitest test suite verifying box output alignment across multiple lines containing emojis and CJK characters, ensuring all rendered lines maintain identical display width.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

🐰 Emojis dance in boxes now,
No jagged edges on my brow!
With stringWidth's careful measure true,
The box adorns each line anew. 📦✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: calculate box width using display width' accurately and concisely describes the main change: switching from character length to display width for box dimension calculations.
Linked Issues check ✅ Passed The PR directly addresses issue #402 by fixing box alignment for wide characters (emojis/CJK) through display width calculations, and includes regression tests verifying the fix.
Out of Scope Changes check ✅ Passed All changes are scoped to the box utility and its tests, directly addressing the display width calculation issue without introducing unrelated modifications.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
test/box.test.ts (1)

5-20: Add a wide-title case to cover the changed title path.

The implementation now changes both content and title width calculations, but the regression test only covers wide content. Adding a title case would protect Lines 263 and 279-282 in src/utils/box.ts.

🧪 Suggested test expansion
 describe("box", () => {
-  test.each(["hello 🚀️ world", "漢字 test"])(
-    "keeps all rendered lines aligned for %s",
-    (text) => {
+  test.each([
+    ["hello 🚀️ world", undefined],
+    ["漢字 test", undefined],
+    ["hello", "🚀️ title"],
+    ["hello", "漢字 title"],
+  ])(
+    "keeps all rendered lines aligned for text %s and title %s",
+    (text, title) => {
       const lines = box(text, {
+        title,
         style: {
           marginTop: 0,
           marginBottom: 0,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/box.test.ts` around lines 5 - 20, Add a test that exercises the
wide-title path by calling the box function with a long/wide title (e.g., a
title containing wide Unicode like "漢字 🚀️") in addition to the existing
wide-content cases so both content and title width code paths are covered;
specifically construct a call to box(...) passing a title option (or the API
variant that accepts a title) and assert that stringWidth on each rendered line
remains equal (reuse the existing test structure and stringWidth assertions) to
protect the logic in the title-width branches touched in src/utils/box.ts (the
title handling in box and related width calculations).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/utils/box.ts`:
- Around line 1-7: The stringWidth helper currently calls _stringWidth
unconditionally which will fail on limited-ICU runtimes; update the stringWidth
function to guard with a runtime check (e.g., typeof Intl !== "undefined" &&
typeof Intl.Segmenter !== "undefined") and only call
_stringWidth(stripAnsi(str)) when the guard passes; otherwise return a safe
fallback such as stripAnsi(str).length (or an equivalent simple character-count)
so callers of stringWidth (referencing the stringWidth function and _stringWidth
import) remain robust in environments without Intl.Segmenter.

---

Nitpick comments:
In `@test/box.test.ts`:
- Around line 5-20: Add a test that exercises the wide-title path by calling the
box function with a long/wide title (e.g., a title containing wide Unicode like
"漢字 🚀️") in addition to the existing wide-content cases so both content and
title width code paths are covered; specifically construct a call to box(...)
passing a title option (or the API variant that accepts a title) and assert that
stringWidth on each rendered line remains equal (reuse the existing test
structure and stringWidth assertions) to protect the logic in the title-width
branches touched in src/utils/box.ts (the title handling in box and related
width calculations).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 370354a5-c2aa-4e96-9075-1624959b5c15

📥 Commits

Reviewing files that changed from the base of the PR and between c47faac and 4b90860.

📒 Files selected for processing (2)
  • src/utils/box.ts
  • test/box.test.ts

Comment thread src/utils/box.ts
Comment on lines +1 to +7
import _stringWidth from "string-width";
import { getColor } from "./color";
import { stripAnsi } from "./string";

function stringWidth(str: string) {
return _stringWidth(stripAnsi(str));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

# Verify all string-width call sites and whether they consistently guard Intl.Segmenter.
rg -n -C3 --type=ts '(_stringWidth|stringWidth\s*\(|Intl\.Segmenter)'

echo
# Verify declared string-width version and Node engine constraints.
fd -i '^(package\.json|pnpm-lock\.yaml)$' -d 3 -x sh -c '
  echo "--- $1"
  rg -n '\''(string-width|"node"|engines)'\'' "$1" || true
' sh {}

Repository: unjs/consola

Length of output: 16465


Add Intl.Segmenter guard to the stringWidth helper in src/utils/box.ts.

The new stringWidth helper calls _stringWidth unconditionally, while src/reporters/fancy.ts protects the same dependency with a runtime guard for missing ICU support. The inconsistency risks runtime failures in limited-ICU runtimes that the project already mitigates elsewhere.

🛡️ Proposed fix
 import _stringWidth from "string-width";
 import { getColor } from "./color";
 import { stripAnsi } from "./string";
 
 function stringWidth(str: string) {
+  // https://github.com/unjs/consola/issues/204
+  if (typeof Intl !== "object" || !Intl.Segmenter) {
+    return stripAnsi(str).length;
+  }
   return _stringWidth(stripAnsi(str));
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import _stringWidth from "string-width";
import { getColor } from "./color";
import { stripAnsi } from "./string";
function stringWidth(str: string) {
return _stringWidth(stripAnsi(str));
}
import _stringWidth from "string-width";
import { getColor } from "./color";
import { stripAnsi } from "./string";
function stringWidth(str: string) {
// https://github.com/unjs/consola/issues/204
if (typeof Intl !== "object" || !Intl.Segmenter) {
return stripAnsi(str).length;
}
return _stringWidth(stripAnsi(str));
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/box.ts` around lines 1 - 7, The stringWidth helper currently calls
_stringWidth unconditionally which will fail on limited-ICU runtimes; update the
stringWidth function to guard with a runtime check (e.g., typeof Intl !==
"undefined" && typeof Intl.Segmenter !== "undefined") and only call
_stringWidth(stripAnsi(str)) when the guard passes; otherwise return a safe
fallback such as stripAnsi(str).length (or an equivalent simple character-count)
so callers of stringWidth (referencing the stringWidth function and _stringWidth
import) remain robust in environments without Intl.Segmenter.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes box() rendering misalignment for wide glyphs (emoji/CJK) by switching width and padding calculations from string length to display width, and adds a regression test to ensure all rendered lines have the same visible width.

Changes:

  • Use string-width (with ANSI stripped) to compute content/title display widths in box().
  • Update title centering and per-line right padding to use display width.
  • Add a Vitest regression test covering emoji and CJK content alignment.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
src/utils/box.ts Switches box sizing/padding logic from .length to display width via string-width.
test/box.test.ts Adds regression test ensuring all rendered box lines have equal display width for wide characters.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/utils/box.ts
import { stripAnsi } from "./string";

function stringWidth(str: string) {
return _stringWidth(stripAnsi(str));
Comment thread src/utils/box.ts
Comment on lines +263 to +266
const titleWidth = opts.title ? stringWidth(opts.title) : 0;
const width =
Math.max(
...textLines.map((line) => stripAnsi(line).length),
opts.title ? stripAnsi(opts.title).length : 0,
) + paddingOffset;
Math.max(...textLines.map((line) => stringWidth(line)), titleWidth) +
paddingOffset;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Using emoji breaks consola.box

2 participants