Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -428,7 +428,8 @@ export function renderGroups(
const bar = renderTeamPickHeader(dm.candidates, dm.focusedIndex, barWidth);
const barPlain = stripAnsi(bar);
// Pad between bar content and suffix to keep suffix right-aligned.
const padLen = Math.max(0, barWidth - barPlain.length);
// Use visibleWidth to account for CJK and multi-code-point graphemes.
const padLen = Math.max(0, barWidth - visibleWidth(barPlain));
const line = pc.dim(REPICK_PREFIX) + bar + " ".repeat(padLen) + pc.dim(REPICK_SUFFIX);
lines.push(clipToWidth(line, termWidth) + "\n");
} else if (opts.teamPickMode?.active) {
Expand Down
53 changes: 53 additions & 0 deletions src/render/team-pick.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,59 @@ describe("renderTeamPickHeader — windowed scrolling", () => {
});
});

// ─── renderTeamPickHeader — Unicode and wide characters ───────────────────────

describe("renderTeamPickHeader — CJK and emoji", () => {
it("renders CJK team names with correct width measurement", () => {
// CJK characters take 2 columns each: "中文" = 4 visible columns
const result = strip(renderTeamPickHeader(["中文", "team"], 0));
expect(result).toContain("[ 中文 ]");
expect(result).toContain("team");
});

it("clips CJK focused team when it exceeds maxWidth", () => {
// "[ 中文测试平台 ]" = 11 chars, 19 visible columns
// With maxWidth=10, should clip to fit
const result = strip(renderTeamPickHeader(["中文测试平台"], 0, 10));
expect(result.length).toBeLessThanOrEqual(10);
// Ensure the ellipsis is present (clipping occurred)
expect(result.endsWith("…")).toBe(true);
});

it("preserves focused ellipsis styling when clipping CJK names", () => {
// Verify the clipped output contains ANSI codes for bold/magenta
// and the ellipsis is styled
const result = renderTeamPickHeader(["中文测试平台"], 0, 10);
expect(result).toMatch(/\x1b\[/); // Has ANSI codes
expect(result).toContain("…"); // Has ellipsis
});

it("renders emoji team names with correct width", () => {
// Single emoji typically takes 2 columns: "🔍" = 2 visible columns
const result = strip(renderTeamPickHeader(["🔍platform", "backend"], 0));
expect(result).toContain("[ 🔍platform ]");
expect(result).toContain("backend");
});

it("handles mixed ASCII and emoji in team names", () => {
const result = strip(renderTeamPickHeader(["data📊", "api", "web🌐"], 1));
expect(result).toContain("[ api ]");
expect(result).toContain("data📊");
expect(result).toContain("web🌐");
});

it("maintains maxWidth constraint with wide characters in window rendering", () => {
// Mix of CJK and ASCII in a windowed render
const teams = ["中", "team", "api", "平"];
// Each CJK = 2 cols, ASCII words = their length
// "[ 中 ]"=5 cols, "team"=4, "api"=3, "平"=2
const maxWidth = 20;
const result = strip(renderTeamPickHeader(teams, 0, maxWidth));
// Verify output length is reasonable (accounting for SEP + ellipsis)
expect(result.length).toBeLessThanOrEqual(maxWidth + 4); // Allow some margin for ANSI codes
});
});

// ─── renderTeamPickHeader — guard conditions ──────────────────────────────────

describe("renderTeamPickHeader — guard conditions", () => {
Expand Down
9 changes: 6 additions & 3 deletions src/render/team-pick.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import pc from "picocolors";
import { visibleWidth, clipToWidth } from "./terminal.ts";

const SEP = " ";

Expand Down Expand Up @@ -45,7 +46,7 @@ export function renderTeamPickHeader(

// Pre-compute visible text for each candidate (focused gets [ ] brackets).
const texts = candidateTeams.map((t, i) => (i === safeIndex ? `[ ${t} ]` : t));
const widths = texts.map((t) => t.length);
const widths = texts.map((t) => visibleWidth(t));
Comment thread
shouze marked this conversation as resolved.
Comment thread
shouze marked this conversation as resolved.

if (maxWidth === undefined) {
// No width constraint — render all candidates.
Expand All @@ -61,8 +62,10 @@ export function renderTeamPickHeader(

// If the focused item alone is wider than maxWidth, clip it.
if (widths[safeIndex] > maxWidth) {
const clipped = texts[safeIndex].slice(0, maxWidth - 1) + "…";
return pc.bold(pc.magenta(clipped));
const styledText = pc.bold(pc.magenta(texts[safeIndex]));
const clipped = clipToWidth(styledText, maxWidth - 1);
const styledEllipsis = pc.bold(pc.magenta("…"));
return clipped + styledEllipsis;
}

// ── Windowed rendering ────────────────────────────────────────────────────
Expand Down
Loading