fix: Banner边框上下对齐 - 加入ANSI可见宽度计算+右侧闭合│+精确padding

- 顶部边框公式修正: innerWidth - titleVisibleLen
- 每行body加右侧闭合│边框
- visibleLength()去除ANSI转义计算真实宽度
- 右侧信息区域自动pad到固定宽度
- Logo宽度调整为20字符

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
pull/1/head
liuzh 1 month ago
parent e7eb5e33b1
commit d8a7171325
  1. 92
      src/main/java/com/claudecode/console/BannerPrinter.java

@ -1,6 +1,7 @@
package com.claudecode.console; package com.claudecode.console;
import java.io.PrintStream; import java.io.PrintStream;
import java.util.regex.Pattern;
/** /**
* Banner 打印器 对应 claude-code/src/components/Banner.tsx * Banner 打印器 对应 claude-code/src/components/Banner.tsx
@ -12,54 +13,30 @@ public class BannerPrinter {
private static final String VERSION = "0.1.0-SNAPSHOT"; private static final String VERSION = "0.1.0-SNAPSHOT";
// 边框字符 // 匹配 ANSI 转义序列的正则
private static final String TL = "╭", TR = "╮", BL = "╰", BR = "╯"; private static final Pattern ANSI_PATTERN = Pattern.compile("\033\\[[0-9;]*m");
private static final String H = "─", V = "│";
/** /**
* 打印带边框的启动 Banner * 打印带边框的启动 Banner
*
* @param out 输出流
* @param provider API 提供者名称
* @param model 模型名称
* @param baseUrl API URL
* @param workDir 工作目录
* @param toolCount 工具数量
* @param cmdCount 命令数量
* @param termInfo 终端信息
*/ */
public static void printBoxed(PrintStream out, String provider, String model, public static void printBoxed(PrintStream out, String provider, String model,
String baseUrl, String workDir, String baseUrl, String workDir,
int toolCount, int cmdCount, String termInfo) { int toolCount, int cmdCount, String termInfo) {
int boxWidth = 90; // 内容宽度(不含左右边框 │)
String hr = H.repeat(boxWidth - 2); int innerWidth = 88;
// Logo(ASCII 冒烟咖啡杯 — 每行精确 16 字符宽 // Logo(ASCII 冒烟咖啡杯 — 每行精确 20 字符宽,含左右空格)
String[] logo = { String[] logo = {
" ) ) ) ", " ) ) ) ",
" ╭────────╮ ", " ╭────────╮ ",
" │ ~~~~~~ │ ", " │ ~~~~~~ │─╮ ",
" │ CLAUDE │ ", " │ CLAUDE │ ",
" │ CODE │ ", " │ CODE │─╯ ",
" ╰─┬────┬─╯ " " ╰─┬────┬─╯ "
}; };
int logoVisualWidth = 20;
// 右侧信息 // 右侧信息(纯可见文本 + ANSI 颜色)
String titleLine = "Claude Code Java v" + VERSION;
String descLine = "Describe a task to get started.";
String tipLine = "Tip: /help for commands, Tab to complete";
// 打印顶部边框
out.println();
out.println(" " + TL + "─── " + AnsiStyle.BOLD + AnsiStyle.BRIGHT_CYAN
+ "Claude Code Java" + AnsiStyle.RESET + AnsiStyle.DIM + " v" + VERSION
+ AnsiStyle.RESET + " " + H.repeat(boxWidth - 28 - VERSION.length()) + TR);
// Logo + 右侧信息(双列布局)
int logoWidth = 16; // logo 视觉宽度
int rightStart = logoWidth + 4;
int contentWidth = boxWidth - 4; // 边框内可用宽度
String[] rightInfo = { String[] rightInfo = {
"", "",
AnsiStyle.BOLD + "Welcome!" + AnsiStyle.RESET, AnsiStyle.BOLD + "Welcome!" + AnsiStyle.RESET,
@ -70,23 +47,50 @@ public class BannerPrinter {
AnsiStyle.DIM + "Tools: " + toolCount + " | Commands: " + cmdCount + " | " + termInfo + AnsiStyle.RESET, AnsiStyle.DIM + "Tools: " + toolCount + " | Commands: " + cmdCount + " | " + termInfo + AnsiStyle.RESET,
}; };
// ── 顶部边框 ──
// 格式: ╭─── Claude Code Java v0.1.0-SNAPSHOT ───...───╮
String title = "Claude Code Java";
String versionStr = " v" + VERSION + " ";
String prefix = "─── ";
// 可见宽度: prefix + title + versionStr + 补充的 ─
int titleVisibleLen = prefix.length() + title.length() + versionStr.length();
int trailingDashes = innerWidth - titleVisibleLen;
if (trailingDashes < 1) trailingDashes = 1;
out.println();
out.println(" ╭" + prefix
+ AnsiStyle.BOLD + AnsiStyle.BRIGHT_CYAN + title + AnsiStyle.RESET
+ AnsiStyle.DIM + versionStr + AnsiStyle.RESET
+ "─".repeat(trailingDashes) + "╮");
// ── 内容行(双列布局) ──
// 每行格式: │ [logo] │ [rightInfo] ... padding ... │
// 中间分隔符 " │ " 占 3 个可见字符
int separatorWidth = 3;
int rightAreaWidth = innerWidth - logoVisualWidth - separatorWidth;
int maxRows = Math.max(logo.length, rightInfo.length); int maxRows = Math.max(logo.length, rightInfo.length);
for (int i = 0; i < maxRows; i++) { for (int i = 0; i < maxRows; i++) {
String leftPart = i < logo.length ? logo[i] : ""; String leftPart = i < logo.length ? logo[i] : "";
String rightPart = i < rightInfo.length ? rightInfo[i] : ""; String rightPart = i < rightInfo.length ? rightInfo[i] : "";
// 左侧 logo 部分(固定宽度,无 ANSI 所以直接 pad) // 左侧 logo(纯文本,直接 pad)
String paddedLeft = padRight(leftPart, logoWidth); String paddedLeft = padRight(leftPart, logoVisualWidth);
// 右侧信息需要计算可见长度,补齐空格到 rightAreaWidth
int rightVisible = visibleLength(rightPart);
int rightPadding = rightAreaWidth - rightVisible;
if (rightPadding < 0) rightPadding = 0;
// 输出行 out.println(" │"
out.println(" " + V + " "
+ AnsiStyle.BRIGHT_CYAN + paddedLeft + AnsiStyle.RESET + AnsiStyle.BRIGHT_CYAN + paddedLeft + AnsiStyle.RESET
+ AnsiStyle.DIM + " │ " + AnsiStyle.RESET + AnsiStyle.DIM + " │ " + AnsiStyle.RESET
+ rightPart); + rightPart + " ".repeat(rightPadding)
+ "│");
} }
// 底部边框 // ── 底部边框 ──
out.println(" " + BL + hr + BR); out.println(" " + "─".repeat(innerWidth) + "╯");
} }
/** /**
@ -100,6 +104,12 @@ public class BannerPrinter {
out.println(); out.println();
} }
/** 计算去除 ANSI 转义后的可见字符宽度 */
private static int visibleLength(String s) {
if (s == null || s.isEmpty()) return 0;
return ANSI_PATTERN.matcher(s).replaceAll("").length();
}
/** 右侧补空格到指定视觉宽度 */ /** 右侧补空格到指定视觉宽度 */
private static String padRight(String s, int width) { private static String padRight(String s, int width) {
int len = s.length(); int len = s.length();

Loading…
Cancel
Save