add ccr statusline command
This commit is contained in:
747
src/utils/statusline.ts
Normal file
747
src/utils/statusline.ts
Normal file
@@ -0,0 +1,747 @@
|
||||
import fs from "node:fs/promises";
|
||||
import { execSync } from "child_process";
|
||||
import path from "node:path";
|
||||
import { CONFIG_FILE, HOME_DIR } from "../constants";
|
||||
import JSON5 from "json5";
|
||||
|
||||
export interface StatusLineModuleConfig {
|
||||
type: string;
|
||||
icon?: string;
|
||||
text: string;
|
||||
color?: string;
|
||||
background?: string;
|
||||
}
|
||||
|
||||
export interface StatusLineThemeConfig {
|
||||
modules: StatusLineModuleConfig[];
|
||||
}
|
||||
|
||||
export interface StatusLineInput {
|
||||
hook_event_name: string;
|
||||
session_id: string;
|
||||
transcript_path: string;
|
||||
cwd: string;
|
||||
model: {
|
||||
id: string;
|
||||
display_name: string;
|
||||
};
|
||||
workspace: {
|
||||
current_dir: string;
|
||||
project_dir: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface AssistantMessage {
|
||||
type: "assistant";
|
||||
message: {
|
||||
model: string;
|
||||
usage: {
|
||||
input_tokens: number;
|
||||
output_tokens: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
// ANSIColor代码
|
||||
const COLORS: Record<string, string> = {
|
||||
reset: "\x1b[0m",
|
||||
bold: "\x1b[1m",
|
||||
dim: "\x1b[2m",
|
||||
// 标准颜色
|
||||
black: "\x1b[30m",
|
||||
red: "\x1b[31m",
|
||||
green: "\x1b[32m",
|
||||
yellow: "\x1b[33m",
|
||||
blue: "\x1b[34m",
|
||||
magenta: "\x1b[35m",
|
||||
cyan: "\x1b[36m",
|
||||
white: "\x1b[37m",
|
||||
// 亮色
|
||||
bright_black: "\x1b[90m",
|
||||
bright_red: "\x1b[91m",
|
||||
bright_green: "\x1b[92m",
|
||||
bright_yellow: "\x1b[93m",
|
||||
bright_blue: "\x1b[94m",
|
||||
bright_magenta: "\x1b[95m",
|
||||
bright_cyan: "\x1b[96m",
|
||||
bright_white: "\x1b[97m",
|
||||
// 背景颜色
|
||||
bg_black: "\x1b[40m",
|
||||
bg_red: "\x1b[41m",
|
||||
bg_green: "\x1b[42m",
|
||||
bg_yellow: "\x1b[43m",
|
||||
bg_blue: "\x1b[44m",
|
||||
bg_magenta: "\x1b[45m",
|
||||
bg_cyan: "\x1b[46m",
|
||||
bg_white: "\x1b[47m",
|
||||
// 亮背景色
|
||||
bg_bright_black: "\x1b[100m",
|
||||
bg_bright_red: "\x1b[101m",
|
||||
bg_bright_green: "\x1b[102m",
|
||||
bg_bright_yellow: "\x1b[103m",
|
||||
bg_bright_blue: "\x1b[104m",
|
||||
bg_bright_magenta: "\x1b[105m",
|
||||
bg_bright_cyan: "\x1b[106m",
|
||||
bg_bright_white: "\x1b[107m",
|
||||
};
|
||||
|
||||
// 使用TrueColor(24位色)支持十六进制颜色
|
||||
const TRUE_COLOR_PREFIX = "\x1b[38;2;";
|
||||
const TRUE_COLOR_BG_PREFIX = "\x1b[48;2;";
|
||||
|
||||
// 将十六进制颜色转为RGB格式
|
||||
function hexToRgb(hex: string): { r: number; g: number; b: number } | null {
|
||||
// 移除#和空格
|
||||
hex = hex.replace(/^#/, '').trim();
|
||||
|
||||
// 处理简写形式 (#RGB -> #RRGGBB)
|
||||
if (hex.length === 3) {
|
||||
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
|
||||
}
|
||||
|
||||
if (hex.length !== 6) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const r = parseInt(hex.substring(0, 2), 16);
|
||||
const g = parseInt(hex.substring(2, 4), 16);
|
||||
const b = parseInt(hex.substring(4, 6), 16);
|
||||
|
||||
// 验证RGB值是否有效
|
||||
if (isNaN(r) || isNaN(g) || isNaN(b) || r < 0 || r > 255 || g < 0 || g > 255 || b < 0 || b > 255) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { r, g, b };
|
||||
}
|
||||
|
||||
// 获取颜色代码
|
||||
function getColorCode(colorName: string): string {
|
||||
// 检查是否是十六进制颜色
|
||||
if (colorName.startsWith('#') || /^[0-9a-fA-F]{6}$/.test(colorName) || /^[0-9a-fA-F]{3}$/.test(colorName)) {
|
||||
const rgb = hexToRgb(colorName);
|
||||
if (rgb) {
|
||||
return `${TRUE_COLOR_PREFIX}${rgb.r};${rgb.g};${rgb.b}m`;
|
||||
}
|
||||
}
|
||||
|
||||
// 默认返回空字符串
|
||||
return "";
|
||||
}
|
||||
|
||||
|
||||
// 变量替换函数,支持{{var}}格式的变量替换
|
||||
function replaceVariables(text: string, variables: Record<string, string>): string {
|
||||
return text.replace(/\{\{(\w+)\}\}/g, (match, varName) => {
|
||||
return variables[varName] || match;
|
||||
});
|
||||
}
|
||||
|
||||
// 默认主题配置 - 使用Nerd Fonts图标和美观配色
|
||||
const DEFAULT_THEME: StatusLineThemeConfig = {
|
||||
modules: [
|
||||
{
|
||||
type: "workDir",
|
||||
icon: "", // nf-md-folder_outline
|
||||
text: "{{workDirName}}",
|
||||
color: "bright_blue"
|
||||
},
|
||||
{
|
||||
type: "gitBranch",
|
||||
icon: "", // nf-dev-git_branch
|
||||
text: "{{gitBranch}}",
|
||||
color: "bright_magenta"
|
||||
},
|
||||
{
|
||||
type: "model",
|
||||
icon: "", // nf-md-robot_outline
|
||||
text: "{{model}}",
|
||||
color: "bright_cyan"
|
||||
},
|
||||
{
|
||||
type: "usage",
|
||||
icon: "↑", // 上箭头
|
||||
text: "{{inputTokens}}",
|
||||
color: "bright_green"
|
||||
},
|
||||
{
|
||||
type: "usage",
|
||||
icon: "↓", // 下箭头
|
||||
text: "{{outputTokens}}",
|
||||
color: "bright_yellow"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// Powerline风格主题配置
|
||||
const POWERLINE_THEME: StatusLineThemeConfig = {
|
||||
modules: [
|
||||
{
|
||||
type: "workDir",
|
||||
icon: "", // nf-md-folder_outline
|
||||
text: "{{workDirName}}",
|
||||
color: "white",
|
||||
background: "bg_bright_blue"
|
||||
},
|
||||
{
|
||||
type: "gitBranch",
|
||||
icon: "", // nf-dev-git_branch
|
||||
text: "{{gitBranch}}",
|
||||
color: "white",
|
||||
background: "bg_bright_magenta"
|
||||
},
|
||||
{
|
||||
type: "model",
|
||||
icon: "", // nf-md-robot_outline
|
||||
text: "{{model}}",
|
||||
color: "white",
|
||||
background: "bg_bright_cyan"
|
||||
},
|
||||
{
|
||||
type: "usage",
|
||||
icon: "↑", // 上箭头
|
||||
text: "{{inputTokens}}",
|
||||
color: "white",
|
||||
background: "bg_bright_green"
|
||||
},
|
||||
{
|
||||
type: "usage",
|
||||
icon: "↓", // 下箭头
|
||||
text: "{{outputTokens}}",
|
||||
color: "white",
|
||||
background: "bg_bright_yellow"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// 简单文本主题配置 - 用于图标无法显示时的fallback
|
||||
const SIMPLE_THEME: StatusLineThemeConfig = {
|
||||
modules: [
|
||||
{
|
||||
type: "workDir",
|
||||
icon: "",
|
||||
text: "{{workDirName}}",
|
||||
color: "bright_blue"
|
||||
},
|
||||
{
|
||||
type: "gitBranch",
|
||||
icon: "",
|
||||
text: "{{gitBranch}}",
|
||||
color: "bright_magenta"
|
||||
},
|
||||
{
|
||||
type: "model",
|
||||
icon: "",
|
||||
text: "{{model}}",
|
||||
color: "bright_cyan"
|
||||
},
|
||||
{
|
||||
type: "usage",
|
||||
icon: "↑",
|
||||
text: "{{inputTokens}}",
|
||||
color: "bright_green"
|
||||
},
|
||||
{
|
||||
type: "usage",
|
||||
icon: "↓",
|
||||
text: "{{outputTokens}}",
|
||||
color: "bright_yellow"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// 格式化usage信息,如果大于1000则使用k单位
|
||||
function formatUsage(input_tokens: number, output_tokens: number): string {
|
||||
if (input_tokens > 1000 || output_tokens > 1000) {
|
||||
const inputFormatted = input_tokens > 1000 ? `${(input_tokens / 1000).toFixed(1)}k` : `${input_tokens}`;
|
||||
const outputFormatted = output_tokens > 1000 ? `${(output_tokens / 1000).toFixed(1)}k` : `${output_tokens}`;
|
||||
return `${inputFormatted} ${outputFormatted}`;
|
||||
}
|
||||
return `${input_tokens} ${output_tokens}`;
|
||||
}
|
||||
|
||||
// 读取用户主目录的主题配置
|
||||
async function getProjectThemeConfig(): Promise<{ theme: StatusLineThemeConfig | null, style: string }> {
|
||||
try {
|
||||
// 只使用主目录的固定配置文件
|
||||
const configPath = CONFIG_FILE;
|
||||
|
||||
// 检查配置文件是否存在
|
||||
try {
|
||||
await fs.access(configPath);
|
||||
} catch {
|
||||
return { theme: null, style: 'default' };
|
||||
}
|
||||
|
||||
const configContent = await fs.readFile(configPath, "utf-8");
|
||||
const config = JSON5.parse(configContent);
|
||||
|
||||
// 检查是否有StatusLine配置
|
||||
if (config.StatusLine) {
|
||||
// 获取当前使用的风格,默认为default
|
||||
const currentStyle = config.StatusLine.currentStyle || 'default';
|
||||
|
||||
// 检查是否有对应风格的配置
|
||||
if (config.StatusLine[currentStyle] && config.StatusLine[currentStyle].modules) {
|
||||
return { theme: config.StatusLine[currentStyle], style: currentStyle };
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// 如果读取失败,返回null
|
||||
// console.error("Failed to read theme config:", error);
|
||||
}
|
||||
|
||||
return { theme: null, style: 'default' };
|
||||
}
|
||||
|
||||
// 检查是否应该使用简单主题(fallback方案)
|
||||
// 当环境变量 USE_SIMPLE_ICONS 被设置时,或者当检测到可能不支持Nerd Fonts的终端时
|
||||
function shouldUseSimpleTheme(): boolean {
|
||||
// 检查环境变量
|
||||
if (process.env.USE_SIMPLE_ICONS === 'true') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查终端类型(一些常见的不支持复杂图标的终端)
|
||||
const term = process.env.TERM || '';
|
||||
const unsupportedTerms = ['dumb', 'unknown'];
|
||||
if (unsupportedTerms.includes(term)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 默认情况下,假设终端支持Nerd Fonts
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查Nerd Fonts图标是否能正确显示
|
||||
// 通过检查终端字体信息或使用试探性方法
|
||||
function canDisplayNerdFonts(): boolean {
|
||||
// 如果环境变量明确指定使用简单图标,则不能显示Nerd Fonts
|
||||
if (process.env.USE_SIMPLE_ICONS === 'true') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查一些常见的支持Nerd Fonts的终端环境变量
|
||||
const fontEnvVars = ['NERD_FONT', 'NERDFONT', 'FONT'];
|
||||
for (const envVar of fontEnvVars) {
|
||||
const value = process.env[envVar];
|
||||
if (value && (value.includes('Nerd') || value.includes('nerd'))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查终端类型
|
||||
const termProgram = process.env.TERM_PROGRAM || '';
|
||||
const supportedTerminals = ['iTerm.app', 'vscode', 'Hyper', 'kitty', 'alacritty'];
|
||||
if (supportedTerminals.includes(termProgram)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查COLORTERM环境变量
|
||||
const colorTerm = process.env.COLORTERM || '';
|
||||
if (colorTerm.includes('truecolor') || colorTerm.includes('24bit')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 默认情况下,假设可以显示Nerd Fonts(但允许用户通过环境变量覆盖)
|
||||
return process.env.USE_SIMPLE_ICONS !== 'true';
|
||||
}
|
||||
|
||||
// 检查特定Unicode字符是否能正确显示
|
||||
// 这是一个简单的试探性检查
|
||||
function canDisplayUnicodeCharacter(char: string): boolean {
|
||||
// 对于Nerd Fonts图标,我们假设支持UTF-8的终端可以显示
|
||||
// 但实际上很难准确检测,所以我们依赖环境变量和终端类型检测
|
||||
try {
|
||||
// 检查终端是否支持UTF-8
|
||||
const lang = process.env.LANG || process.env.LC_ALL || process.env.LC_CTYPE || '';
|
||||
if (lang.includes('UTF-8') || lang.includes('utf8') || lang.includes('UTF8')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查LC_*环境变量
|
||||
const lcVars = ['LC_ALL', 'LC_CTYPE', 'LANG'];
|
||||
for (const lcVar of lcVars) {
|
||||
const value = process.env[lcVar];
|
||||
if (value && (value.includes('UTF-8') || value.includes('utf8'))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// 如果检查失败,默认返回true
|
||||
return true;
|
||||
}
|
||||
|
||||
// 默认情况下,假设可以显示
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function parseStatusLineData(input: StatusLineInput): Promise<string> {
|
||||
try {
|
||||
// 检查是否应该使用简单主题
|
||||
const useSimpleTheme = shouldUseSimpleTheme();
|
||||
|
||||
// 检查是否可以显示Nerd Fonts图标
|
||||
const canDisplayNerd = canDisplayNerdFonts();
|
||||
|
||||
// 确定使用的主题:如果用户强制使用简单主题或无法显示Nerd Fonts,则使用简单主题
|
||||
const effectiveTheme = useSimpleTheme || !canDisplayNerd ? SIMPLE_THEME : DEFAULT_THEME;
|
||||
|
||||
// 获取主目录的主题配置,如果没有则使用确定的默认配置
|
||||
const { theme: projectTheme, style: currentStyle } = await getProjectThemeConfig();
|
||||
const theme = projectTheme || effectiveTheme;
|
||||
|
||||
// 获取当前工作目录和Git分支
|
||||
const workDir = input.workspace.current_dir;
|
||||
let gitBranch = "";
|
||||
|
||||
try {
|
||||
// 尝试获取Git分支名
|
||||
gitBranch = execSync("git branch --show-current", {
|
||||
cwd: workDir,
|
||||
stdio: ["pipe", "pipe", "ignore"],
|
||||
})
|
||||
.toString()
|
||||
.trim();
|
||||
} catch (error) {
|
||||
// 如果不是Git仓库或获取失败,则忽略错误
|
||||
}
|
||||
|
||||
// 从transcript_path文件中读取最后一条assistant消息
|
||||
const transcriptContent = await fs.readFile(input.transcript_path, "utf-8");
|
||||
const lines = transcriptContent.trim().split("\n");
|
||||
|
||||
// 反向遍历寻找最后一条assistant消息
|
||||
let model = "";
|
||||
let inputTokens = 0;
|
||||
let outputTokens = 0;
|
||||
|
||||
for (let i = lines.length - 1; i >= 0; i--) {
|
||||
try {
|
||||
const message: AssistantMessage = JSON.parse(lines[i]);
|
||||
if (message.type === "assistant" && message.message.model) {
|
||||
model = message.message.model;
|
||||
|
||||
if (message.message.usage) {
|
||||
inputTokens = message.message.usage.input_tokens;
|
||||
outputTokens = message.message.usage.output_tokens;
|
||||
}
|
||||
break;
|
||||
}
|
||||
} catch (parseError) {
|
||||
// 忽略解析错误,继续查找
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有从transcript中获取到模型名称,则尝试从配置文件中获取
|
||||
if (!model) {
|
||||
try {
|
||||
// 获取项目配置文件路径
|
||||
const projectConfigPath = path.join(workDir, ".claude-code-router", "config.json");
|
||||
let configPath = projectConfigPath;
|
||||
|
||||
// 检查项目配置文件是否存在,如果不存在则使用用户主目录的配置文件
|
||||
try {
|
||||
await fs.access(projectConfigPath);
|
||||
} catch {
|
||||
configPath = CONFIG_FILE;
|
||||
}
|
||||
|
||||
// 读取配置文件
|
||||
const configContent = await fs.readFile(configPath, "utf-8");
|
||||
const config = JSON5.parse(configContent);
|
||||
|
||||
// 从Router字段的default内容中获取模型名称
|
||||
if (config.Router && config.Router.default) {
|
||||
const [, defaultModel] = config.Router.default.split(",");
|
||||
if (defaultModel) {
|
||||
model = defaultModel.trim();
|
||||
}
|
||||
}
|
||||
} catch (configError) {
|
||||
// 如果配置文件读取失败,则忽略错误
|
||||
}
|
||||
}
|
||||
|
||||
// 如果仍然没有获取到模型名称,则使用传入的JSON数据中的model字段的display_name
|
||||
if (!model) {
|
||||
model = input.model.display_name;
|
||||
}
|
||||
|
||||
// 获取工作目录名
|
||||
const workDirName = workDir.split("/").pop() || "";
|
||||
|
||||
// 格式化usage信息
|
||||
const usage = formatUsage(inputTokens, outputTokens);
|
||||
const [formattedInputTokens, formattedOutputTokens] = usage.split(" ");
|
||||
|
||||
// 定义变量替换映射
|
||||
const variables = {
|
||||
workDirName,
|
||||
gitBranch,
|
||||
model,
|
||||
inputTokens: formattedInputTokens,
|
||||
outputTokens: formattedOutputTokens
|
||||
};
|
||||
|
||||
// 确定使用的风格
|
||||
const isPowerline = currentStyle === 'powerline';
|
||||
|
||||
// 根据风格渲染状态行
|
||||
if (isPowerline) {
|
||||
return renderPowerlineStyle(theme, variables);
|
||||
} else {
|
||||
return renderDefaultStyle(theme, variables);
|
||||
}
|
||||
} catch (error) {
|
||||
// 发生错误时返回空字符串
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
// 读取用户主目录的主题配置(指定风格)
|
||||
async function getProjectThemeConfigForStyle(style: string): Promise<StatusLineThemeConfig | null> {
|
||||
try {
|
||||
// 只使用主目录的固定配置文件
|
||||
const configPath = CONFIG_FILE;
|
||||
|
||||
// 检查配置文件是否存在
|
||||
try {
|
||||
await fs.access(configPath);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
const configContent = await fs.readFile(configPath, "utf-8");
|
||||
const config = JSON5.parse(configContent);
|
||||
|
||||
// 检查是否有StatusLine配置
|
||||
if (config.StatusLine && config.StatusLine[style] && config.StatusLine[style].modules) {
|
||||
return config.StatusLine[style];
|
||||
}
|
||||
} catch (error) {
|
||||
// 如果读取失败,返回null
|
||||
// console.error("Failed to read theme config:", error);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// 渲染默认风格的状态行
|
||||
function renderDefaultStyle(
|
||||
theme: StatusLineThemeConfig,
|
||||
variables: Record<string, string>
|
||||
): string {
|
||||
const modules = theme.modules || DEFAULT_THEME.modules;
|
||||
const parts: string[] = [];
|
||||
|
||||
// 遍历模块数组,渲染每个模块
|
||||
for (let i = 0; i < Math.min(modules.length, 5); i++) {
|
||||
const module = modules[i];
|
||||
const color = module.color ? getColorCode(module.color) : "";
|
||||
const background = module.background ? getColorCode(module.background) : "";
|
||||
const icon = module.icon || "";
|
||||
const text = replaceVariables(module.text, variables);
|
||||
|
||||
// 如果text为空且不是usage类型,则跳过该模块
|
||||
if (!text && module.type !== "usage") {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 构建模块字符串
|
||||
let part = `${background}${color}`;
|
||||
if (icon) {
|
||||
part += `${icon} `;
|
||||
}
|
||||
part += `${text}${COLORS.reset}`;
|
||||
|
||||
parts.push(part);
|
||||
}
|
||||
|
||||
// 使用空格连接所有部分
|
||||
return parts.join(" ");
|
||||
}
|
||||
|
||||
// Powerline符号
|
||||
const SEP_RIGHT = "\uE0B0"; //
|
||||
|
||||
// 颜色编号(256色表)
|
||||
const COLOR_MAP: Record<string, number> = {
|
||||
// 基础颜色映射到256色
|
||||
black: 0,
|
||||
red: 1,
|
||||
green: 2,
|
||||
yellow: 3,
|
||||
blue: 4,
|
||||
magenta: 5,
|
||||
cyan: 6,
|
||||
white: 7,
|
||||
bright_black: 8,
|
||||
bright_red: 9,
|
||||
bright_green: 10,
|
||||
bright_yellow: 11,
|
||||
bright_blue: 12,
|
||||
bright_magenta: 13,
|
||||
bright_cyan: 14,
|
||||
bright_white: 15,
|
||||
// 亮背景色映射
|
||||
bg_black: 0,
|
||||
bg_red: 1,
|
||||
bg_green: 2,
|
||||
bg_yellow: 3,
|
||||
bg_blue: 4,
|
||||
bg_magenta: 5,
|
||||
bg_cyan: 6,
|
||||
bg_white: 7,
|
||||
bg_bright_black: 8,
|
||||
bg_bright_red: 9,
|
||||
bg_bright_green: 10,
|
||||
bg_bright_yellow: 11,
|
||||
bg_bright_blue: 12,
|
||||
bg_bright_magenta: 13,
|
||||
bg_bright_cyan: 14,
|
||||
bg_bright_white: 15,
|
||||
// 自定义颜色映射
|
||||
bg_bright_orange: 202,
|
||||
bg_bright_purple: 129,
|
||||
};
|
||||
|
||||
// 获取TrueColor的RGB值
|
||||
function getTrueColorRgb(colorName: string): { r: number; g: number; b: number } | null {
|
||||
// 如果是预定义颜色,返回对应RGB
|
||||
if (COLOR_MAP[colorName] !== undefined) {
|
||||
const color256 = COLOR_MAP[colorName];
|
||||
return color256ToRgb(color256);
|
||||
}
|
||||
|
||||
// 处理十六进制颜色
|
||||
if (colorName.startsWith('#') || /^[0-9a-fA-F]{6}$/.test(colorName) || /^[0-9a-fA-F]{3}$/.test(colorName)) {
|
||||
return hexToRgb(colorName);
|
||||
}
|
||||
|
||||
// 处理背景色十六进制
|
||||
if (colorName.startsWith('bg_#')) {
|
||||
return hexToRgb(colorName.substring(3));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// 将256色表索引转换为RGB值
|
||||
function color256ToRgb(index: number): { r: number; g: number; b: number } | null {
|
||||
if (index < 0 || index > 255) return null;
|
||||
|
||||
// ANSI 256色表转换
|
||||
if (index < 16) {
|
||||
// 基本颜色
|
||||
const basicColors = [
|
||||
[0, 0, 0], [128, 0, 0], [0, 128, 0], [128, 128, 0],
|
||||
[0, 0, 128], [128, 0, 128], [0, 128, 128], [192, 192, 192],
|
||||
[128, 128, 128], [255, 0, 0], [0, 255, 0], [255, 255, 0],
|
||||
[0, 0, 255], [255, 0, 255], [0, 255, 255], [255, 255, 255]
|
||||
];
|
||||
return { r: basicColors[index][0], g: basicColors[index][1], b: basicColors[index][2] };
|
||||
} else if (index < 232) {
|
||||
// 216色:6×6×6的颜色立方体
|
||||
const i = index - 16;
|
||||
const r = Math.floor(i / 36);
|
||||
const g = Math.floor((i % 36) / 6);
|
||||
const b = i % 6;
|
||||
const rgb = [0, 95, 135, 175, 215, 255];
|
||||
return { r: rgb[r], g: rgb[g], b: rgb[b] };
|
||||
} else {
|
||||
// 灰度色
|
||||
const gray = 8 + (index - 232) * 10;
|
||||
return { r: gray, g: gray, b: gray };
|
||||
}
|
||||
}
|
||||
|
||||
// 生成一个无缝拼接的段:文本在 bgN 上显示,分隔符从 bgN 过渡到 nextBgN
|
||||
function segment(text: string, textFg: string, bgColor: string, nextBgColor: string | null): string {
|
||||
const bgRgb = getTrueColorRgb(bgColor);
|
||||
if (!bgRgb) {
|
||||
// 如果无法获取RGB,使用默认蓝色背景
|
||||
const defaultBlueRgb = { r: 33, g: 150, b: 243 };
|
||||
const curBg = `\x1b[48;2;${defaultBlueRgb.r};${defaultBlueRgb.g};${defaultBlueRgb.b}m`;
|
||||
const fgColor = `\x1b[38;2;255;255;255m`;
|
||||
const body = `${curBg}${fgColor} ${text} \x1b[0m`;
|
||||
return body;
|
||||
}
|
||||
|
||||
const curBg = `\x1b[48;2;${bgRgb.r};${bgRgb.g};${bgRgb.b}m`;
|
||||
|
||||
// 获取前景色RGB
|
||||
let fgRgb = { r: 255, g: 255, b: 255 }; // 默认前景色为白色
|
||||
const textFgRgb = getTrueColorRgb(textFg);
|
||||
if (textFgRgb) {
|
||||
fgRgb = textFgRgb;
|
||||
}
|
||||
|
||||
const fgColor = `\x1b[38;2;${fgRgb.r};${fgRgb.g};${fgRgb.b}m`;
|
||||
const body = `${curBg}${fgColor} ${text} \x1b[0m`;
|
||||
|
||||
if (nextBgColor != null) {
|
||||
const nextBgRgb = getTrueColorRgb(nextBgColor);
|
||||
if (nextBgRgb) {
|
||||
// 分隔符:前景色是当前段的背景色,背景色是下一段的背景色
|
||||
const sepCurFg = `\x1b[38;2;${bgRgb.r};${bgRgb.g};${bgRgb.b}m`;
|
||||
const sepNextBg = `\x1b[48;2;${nextBgRgb.r};${nextBgRgb.g};${nextBgRgb.b}m`;
|
||||
const sep = `${sepCurFg}${sepNextBg}${SEP_RIGHT}\x1b[0m`;
|
||||
return body + sep;
|
||||
}
|
||||
// 如果没有下一个背景色,假设终端背景为黑色并渲染黑色箭头
|
||||
const sepCurFg = `\x1b[38;2;${bgRgb.r};${bgRgb.g};${bgRgb.b}m`;
|
||||
const sepNextBg = `\x1b[48;2;0;0;0m`; // 黑色背景
|
||||
const sep = `${sepCurFg}${sepNextBg}${SEP_RIGHT}\x1b[0m`;
|
||||
return body + sep;
|
||||
}
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
// 渲染Powerline风格的状态行
|
||||
function renderPowerlineStyle(
|
||||
theme: StatusLineThemeConfig,
|
||||
variables: Record<string, string>
|
||||
): string {
|
||||
const modules = theme.modules || POWERLINE_THEME.modules;
|
||||
const segments: string[] = [];
|
||||
|
||||
// 遍历模块数组,渲染每个模块
|
||||
for (let i = 0; i < Math.min(modules.length, 5); i++) {
|
||||
const module = modules[i];
|
||||
const color = module.color || "white";
|
||||
const backgroundName = module.background || "";
|
||||
const icon = module.icon || "";
|
||||
const text = replaceVariables(module.text, variables);
|
||||
|
||||
// 如果text为空且不是usage类型,则跳过该模块
|
||||
if (!text && module.type !== "usage") {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 构建显示文本
|
||||
let displayText = "";
|
||||
if (icon) {
|
||||
displayText += `${icon} `;
|
||||
}
|
||||
displayText += text;
|
||||
|
||||
// 获取下一个模块的背景色(用于分隔符)
|
||||
let nextBackground: string | null = null;
|
||||
if (i < modules.length - 1) {
|
||||
const nextModule = modules[i + 1];
|
||||
nextBackground = nextModule.background || null;
|
||||
}
|
||||
|
||||
// 使用模块定义的背景色,或者为Powerline风格提供默认背景色
|
||||
const actualBackground = backgroundName || "bg_bright_blue";
|
||||
|
||||
// 生成段,支持十六进制颜色
|
||||
const segmentStr = segment(displayText, color, actualBackground, nextBackground);
|
||||
segments.push(segmentStr);
|
||||
}
|
||||
|
||||
return segments.join("");
|
||||
}
|
||||
Reference in New Issue
Block a user