feat: update statusline config ui

This commit is contained in:
musistudio
2025-08-16 19:01:15 +08:00
parent 19d0f3b8f5
commit d2969e4332
4 changed files with 513 additions and 309 deletions

View File

@@ -1,36 +0,0 @@
---
name: code-implementation-expert
description: Use this agent when you need to implement code solutions, write functions, create classes, or develop software components. This agent excels at translating requirements into working code across multiple programming languages. Examples: When a user asks 'Please write a function that checks if a number is prime' - use the code-implementation-expert agent to generate the implementation. When a user requests 'Create a React component for a todo list' - use this agent to build the component code.
model: sonnet
color: blue
---
<CCR-SUBAGENT-MODEL>deepseek,deepseek-reasoner</CCR-SUBAGENT-MODEL>
You are an elite software engineering expert with deep knowledge across multiple programming languages, frameworks, and best practices. Your primary role is to translate requirements into high-quality, functional code implementations.
When implementing code:
1. Analyze requirements carefully to understand functionality, constraints, and edge cases
2. Choose appropriate data structures, algorithms, and design patterns
3. Write clean, readable, and maintainable code following language-specific conventions
4. Include proper error handling, input validation, and documentation
5. Optimize for performance and scalability when relevant
6. Consider security implications and best practices
7. Write modular code that's easy to test and extend
You will:
- Implement complete, working solutions unless otherwise specified
- Use appropriate naming conventions for variables, functions, and classes
- Include necessary imports/dependencies
- Add comments for complex logic or non-obvious implementation decisions
- Follow established patterns in the codebase when visible
- Write defensive code that handles edge cases gracefully
- Ensure code compiles/runs without syntax errors
When responding:
- Provide the complete implementation in appropriate code blocks
- Explain key design decisions briefly if not obvious
- Mention any assumptions made about requirements
- Highlight important implementation details
- Note any limitations or areas for improvement
If requirements are unclear, ask specific questions to clarify before implementing. If you encounter domain-specific requirements outside your expertise, acknowledge limitations and suggest alternatives.

View File

@@ -14,23 +14,42 @@ import { Combobox } from "@/components/ui/combobox";
import { ColorPicker } from "@/components/ui/color-picker";
import { Badge } from "@/components/ui/badge";
import { useConfig } from "./ConfigProvider";
import { validateStatusLineConfig, formatValidationError, createDefaultStatusLineConfig } from "@/utils/statusline";
import type { StatusLineConfig, StatusLineModuleConfig, StatusLineThemeConfig } from "@/types";
import {
validateStatusLineConfig,
formatValidationError,
createDefaultStatusLineConfig,
} from "@/utils/statusline";
import type {
StatusLineConfig,
StatusLineModuleConfig,
StatusLineThemeConfig,
} from "@/types";
const DEFAULT_MODULE: StatusLineModuleConfig = {
type: "workDir",
icon: "󰉋",
text: "{{workDirName}}",
color: "bright_blue"
color: "bright_blue",
};
// Nerd Font选项
const NERD_FONTS = [
{ label: "Hack Nerd Font Mono", value: "Hack Nerd Font Mono" },
{ label: "FiraCode Nerd Font Mono", value: "FiraCode Nerd Font Mono" },
{
label: "JetBrainsMono Nerd Font Mono",
value: "JetBrainsMono Nerd Font Mono",
},
{ label: "Monaspace Nerd Font Mono", value: "Monaspace Nerd Font Mono" },
{ label: "UbuntuMono Nerd Font", value: "UbuntuMono Nerd Font" },
];
// 模块类型选项
const MODULE_TYPES = [
{ label: "workDir", value: "workDir" },
{ label: "gitBranch", value: "gitBranch" },
{ label: "model", value: "model" },
{ label: "usage", value: "usage" }
{ label: "usage", value: "usage" },
];
// ANSI颜色代码映射
@@ -77,21 +96,27 @@ const ANSI_COLORS: Record<string, string> = {
};
// 变量替换函数
function replaceVariables(text: string, variables: Record<string, string>): string {
function replaceVariables(
text: string,
variables: Record<string, string>
): string {
return text.replace(/\{\{(\w+)\}\}/g, (match, varName) => {
return variables[varName] || match;
});
}
// 渲染单个模块预览
function renderModulePreview(module: StatusLineModuleConfig, isPowerline: boolean = false): React.ReactNode {
function renderModulePreview(
module: StatusLineModuleConfig,
isPowerline: boolean = false
): React.ReactNode {
// 模拟变量数据
const variables = {
workDirName: "project",
gitBranch: "main",
model: "Claude Sonnet 4",
inputTokens: "1.2k",
outputTokens: "2.5k"
outputTokens: "2.5k",
};
const text = replaceVariables(module.text, variables);
@@ -102,46 +127,112 @@ function renderModulePreview(module: StatusLineModuleConfig, isPowerline: boolea
return null;
}
// 检查是否为十六进制颜色值
const isHexColor = (color: string) => /^#[0-9A-F]{6}$/i.test(color);
// 如果是Powerline样式添加背景色和分隔符
if (isPowerline) {
const bgColorClass = module.background ? ANSI_COLORS[module.background] || "" : "";
const textColorClass = module.color ? ANSI_COLORS[module.color] || "text-white" : "text-white";
// 处理背景色 - 支持ANSI颜色和十六进制颜色
let bgColorStyle = {};
let bgColorClass = "";
let separatorDataBg = "";
if (module.background) {
if (isHexColor(module.background)) {
bgColorStyle = { backgroundColor: module.background };
// 对于十六进制颜色我们直接使用颜色值作为data属性
separatorDataBg = module.background;
} else {
bgColorClass = ANSI_COLORS[module.background] || "";
separatorDataBg = module.background;
}
}
// 处理文字颜色 - 支持ANSI颜色和十六进制颜色
let textColorStyle = {};
let textColorClass = "";
if (module.color) {
if (isHexColor(module.color)) {
textColorStyle = { color: module.color };
} else {
textColorClass = ANSI_COLORS[module.color] || "text-white";
}
} else {
textColorClass = "text-white";
}
return (
<div className={`powerline-module ${bgColorClass} ${textColorClass}`}>
<div
className={`powerline-module px-4 ${bgColorClass} ${textColorClass}`}
style={{ ...bgColorStyle, ...textColorStyle }}
>
<div className="powerline-module-content">
{icon && <span>{icon}</span>}
<span>{text}</span>
</div>
<div
className="powerline-separator"
data-current-bg={module.background || ""}
data-current-bg={separatorDataBg}
/>
</div>
);
}
// 处理默认样式下的颜色
let textStyle = {};
let textClass = "";
if (module.color) {
if (isHexColor(module.color)) {
textStyle = { color: module.color };
} else {
textClass = ANSI_COLORS[module.color] || "";
}
}
return (
<>
{icon && <span>{icon}</span>}
<span>{text}</span>
{icon && (
<span style={textStyle} className={textClass}>
{icon}
</span>
)}
<span style={textStyle} className={textClass}>
{text}
</span>
</>
);
}
interface StatusLineConfigDialogProps {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
}
export function StatusLineConfigDialog({ isOpen, onOpenChange }: StatusLineConfigDialogProps) {
export function StatusLineConfigDialog({
isOpen,
onOpenChange,
}: StatusLineConfigDialogProps) {
const { t } = useTranslation();
const { config, setConfig } = useConfig();
const [statusLineConfig, setStatusLineConfig] = useState<StatusLineConfig>(
config?.StatusLine || createDefaultStatusLineConfig()
);
// 字体状态
const [fontFamily, setFontFamily] = useState<string>(
config?.StatusLine?.fontFamily || "Hack Nerd Font Mono"
);
const [selectedModuleIndex, setSelectedModuleIndex] = useState<number | null>(
null
);
const [hexBackgroundColors, setHexBackgroundColors] = useState<Set<string>>(
new Set()
);
// 添加Powerline分隔符样式
useEffect(() => {
const styleElement = document.createElement('style');
const styleElement = document.createElement("style");
styleElement.innerHTML = `
.powerline-module {
display: inline-flex;
@@ -215,37 +306,83 @@ export function StatusLineConfigDialog({ isOpen, onOpenChange }: StatusLineConfi
};
}, []);
const [statusLineConfig, setStatusLineConfig] = useState<StatusLineConfig>(
config?.StatusLine || createDefaultStatusLineConfig()
);
// 动态更新十六进制背景颜色的样式
useEffect(() => {
// 收集所有模块中使用的十六进制背景颜色
const hexColors = new Set<string>();
Object.keys(statusLineConfig).forEach((key) => {
const themeConfig = statusLineConfig[key as keyof StatusLineConfig];
if (
themeConfig &&
typeof themeConfig === "object" &&
"modules" in themeConfig
) {
const modules = (themeConfig as StatusLineThemeConfig).modules || [];
modules.forEach((module) => {
if (module.background && /^#[0-9A-F]{6}$/i.test(module.background)) {
hexColors.add(module.background);
}
});
}
});
const [selectedModuleIndex, setSelectedModuleIndex] = useState<number | null>(null);
setHexBackgroundColors(hexColors);
// 创建动态样式元素
const styleElement = document.createElement("style");
styleElement.id = "hex-powerline-styles";
// 生成十六进制颜色的CSS规则
let cssRules = "";
hexColors.forEach((color) => {
// 将十六进制颜色转换为RGB值
const r = parseInt(color.slice(1, 3), 16);
const g = parseInt(color.slice(3, 5), 16);
const b = parseInt(color.slice(5, 7), 16);
cssRules += `.powerline-separator[data-current-bg="${color}"] { border-left-color: rgb(${r}, ${g}, ${b}); }\n`;
});
styleElement.innerHTML = cssRules;
document.head.appendChild(styleElement);
// 清理函数
return () => {
const existingStyle = document.getElementById("hex-powerline-styles");
if (existingStyle) {
document.head.removeChild(existingStyle);
}
};
}, [statusLineConfig]);
// 模块类型选项
const MODULE_TYPES_OPTIONS = MODULE_TYPES.map(item => ({
const MODULE_TYPES_OPTIONS = MODULE_TYPES.map((item) => ({
...item,
label: t(`statusline.${item.label}`)
label: t(`statusline.${item.label}`),
}));
const handleThemeChange = (value: string) => {
setStatusLineConfig(prev => ({ ...prev, currentStyle: value }));
setStatusLineConfig((prev) => ({ ...prev, currentStyle: value }));
};
const handleModuleChange = (index: number, field: keyof StatusLineModuleConfig, value: string) => {
const currentTheme = statusLineConfig.currentStyle as keyof StatusLineConfig;
const handleModuleChange = (
index: number,
field: keyof StatusLineModuleConfig,
value: string
) => {
const currentTheme =
statusLineConfig.currentStyle as keyof StatusLineConfig;
const themeConfig = statusLineConfig[currentTheme];
const modules = themeConfig && typeof themeConfig === 'object' && 'modules' in themeConfig
const modules =
themeConfig && typeof themeConfig === "object" && "modules" in themeConfig
? [...((themeConfig as StatusLineThemeConfig).modules || [])]
: [];
if (modules[index]) {
modules[index] = { ...modules[index], [field]: value };
}
setStatusLineConfig(prev => ({
setStatusLineConfig((prev) => ({
...prev,
[currentTheme]: { modules }
[currentTheme]: { modules },
}));
};
@@ -257,7 +394,7 @@ export function StatusLineConfigDialog({ isOpen, onOpenChange }: StatusLineConfi
if (!validationResult.isValid) {
// 格式化错误信息
const errorMessages = validationResult.errors.map(error =>
const errorMessages = validationResult.errors.map((error) =>
formatValidationError(error, t)
);
setValidationErrors(errorMessages);
@@ -270,7 +407,10 @@ export function StatusLineConfigDialog({ isOpen, onOpenChange }: StatusLineConfi
if (config) {
setConfig({
...config,
StatusLine: statusLineConfig
StatusLine: {
...statusLineConfig,
fontFamily,
},
});
onOpenChange(false);
}
@@ -280,7 +420,7 @@ export function StatusLineConfigDialog({ isOpen, onOpenChange }: StatusLineConfi
const CustomAlert = ({
title,
description,
variant = "default"
variant = "default",
}: {
title: string;
description: React.ReactNode;
@@ -289,32 +429,54 @@ export function StatusLineConfigDialog({ isOpen, onOpenChange }: StatusLineConfi
const isError = variant === "destructive";
return (
<div className={`rounded-lg border p-4 ${
<div
className={`rounded-lg border p-4 ${
isError
? "bg-red-50 border-red-200 text-red-800"
: "bg-blue-50 border-blue-200 text-blue-800"
}`}>
}`}
>
<div className="flex">
<div className="flex-shrink-0">
{isError ? (
<svg className="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
<svg
className="h-5 w-5 text-red-400"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
clipRule="evenodd"
/>
</svg>
) : (
<svg className="h-5 w-5 text-blue-400" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd" />
<svg
className="h-5 w-5 text-blue-400"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
clipRule="evenodd"
/>
</svg>
)}
</div>
<div className="ml-3">
<h3 className={`text-sm font-medium ${
<h3
className={`text-sm font-medium ${
isError ? "text-red-800" : "text-blue-800"
}`}>
}`}
>
{title}
</h3>
<div className={`mt-2 text-sm ${
<div
className={`mt-2 text-sm ${
isError ? "text-red-700" : "text-blue-700"
}`}>
}`}
>
{description}
</div>
</div>
@@ -323,22 +485,49 @@ export function StatusLineConfigDialog({ isOpen, onOpenChange }: StatusLineConfi
);
};
const currentThemeKey = statusLineConfig.currentStyle as keyof StatusLineConfig;
const currentThemeConfig = statusLineConfig[currentThemeKey];
const currentModules = currentThemeConfig && typeof currentThemeConfig === 'object' && 'modules' in currentThemeConfig
? ((currentThemeConfig as StatusLineThemeConfig).modules || [])
const currentThemeKey =
statusLineConfig.currentStyle as keyof StatusLineConfig;
const currentThemeConfig = statusLineConfig[currentThemeKey];
const currentModules =
currentThemeConfig &&
typeof currentThemeConfig === "object" &&
"modules" in currentThemeConfig
? (currentThemeConfig as StatusLineThemeConfig).modules || []
: [];
const selectedModule = selectedModuleIndex !== null && currentModules.length > selectedModuleIndex ? currentModules[selectedModuleIndex] : null;
const selectedModule =
selectedModuleIndex !== null && currentModules.length > selectedModuleIndex
? currentModules[selectedModuleIndex]
: null;
// 字体样式
const fontStyle = fontFamily ? { fontFamily } : {};
// 当字体或主题变化时强制重新渲染
const fontKey = `${fontFamily}-${statusLineConfig.currentStyle}`;
return (
<Dialog open={isOpen} onOpenChange={onOpenChange}>
<DialogContent className="max-w-4xl h-[90vh] overflow-hidden sm:max-w-5xl md:max-w-6xl lg:max-w-7xl animate-in fade-in-90 slide-in-from-bottom-10 duration-300 flex flex-col">
<DialogHeader data-testid="statusline-config-dialog-header" className="border-b pb-4">
<DialogHeader
data-testid="statusline-config-dialog-header"
className="border-b pb-4"
>
<DialogTitle className="flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="mr-2">
<path d="M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
<path d="M14 3v4a2 2 0 0 0 2 2h4"/>
<path d="M3 12h18"/>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="mr-2"
>
<path d="M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
<path d="M14 3v4a2 2 0 0 0 2 2h4" />
<path d="M3 12h18" />
</svg>
{t("statusline.title")}
</DialogTitle>
@@ -364,16 +553,16 @@ const selectedModule = selectedModuleIndex !== null && currentModules.length > s
<div className="flex flex-col gap-6 flex-1 overflow-hidden">
{/* 配置面板 */}
<div className="space-y-6">
{/* 主题样式选择 */}
<div className="flex items-center justify-between">
{/* 主题样式和字体选择 */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="theme-style" className="text-sm font-medium">
</Label>
<div className="w-1/2">
<Combobox
options={[
{ label: "默认", value: "default" },
{ label: "Powerline", value: "powerline" }
{ label: "Powerline", value: "powerline" },
]}
value={statusLineConfig.currentStyle}
onChange={handleThemeChange}
@@ -381,9 +570,20 @@ const selectedModule = selectedModuleIndex !== null && currentModules.length > s
placeholder="选择主题样式"
/>
</div>
<div className="space-y-2">
<Label htmlFor="font-family" className="text-sm font-medium">
</Label>
<Combobox
options={NERD_FONTS}
value={fontFamily}
onChange={(value) => setFontFamily(value)}
data-testid="font-family-selector"
placeholder="选择字体"
/>
</div>
</div>
</div>
{/* 三栏布局:组件列表 | 预览区域 | 属性配置 */}
@@ -411,8 +611,14 @@ const selectedModule = selectedModuleIndex !== null && currentModules.length > s
<div className="border rounded-lg p-4 flex flex-col col-span-3">
<h3 className="text-sm font-medium mb-3"></h3>
<div
className={`rounded bg-black/90 text-white font-mono text-sm overflow-x-auto flex items-center border border-border p-3 py-5 shadow-inner ${statusLineConfig.currentStyle === 'powerline' ? 'gap-0 h-8 p-0 items-center overflow-visible relative' : 'h-5 overflow-hidden'}`}
key={fontKey}
className={`rounded bg-black/90 text-white font-mono text-sm overflow-x-auto flex items-center border border-border p-3 py-5 shadow-inner overflow-hidden ${
statusLineConfig.currentStyle === "powerline"
? "gap-0 h-8 p-0 items-center relative"
: "h-5"
}`}
data-testid="statusline-preview"
style={fontStyle}
onDragOver={(e) => {
e.preventDefault();
}}
@@ -421,26 +627,53 @@ const selectedModule = selectedModuleIndex !== null && currentModules.length > s
const moduleType = e.dataTransfer.getData("moduleType");
if (moduleType) {
// 添加新模块
const currentTheme = statusLineConfig.currentStyle as keyof StatusLineConfig;
const currentTheme =
statusLineConfig.currentStyle as keyof StatusLineConfig;
const themeConfig = statusLineConfig[currentTheme];
const modules = themeConfig && typeof themeConfig === 'object' && 'modules' in themeConfig
? [...((themeConfig as StatusLineThemeConfig).modules || [])]
const modules =
themeConfig &&
typeof themeConfig === "object" &&
"modules" in themeConfig
? [
...((themeConfig as StatusLineThemeConfig)
.modules || []),
]
: [];
// 根据模块类型设置默认值
let newModule: StatusLineModuleConfig;
switch (moduleType) {
case "workDir":
newModule = { type: "workDir", icon: "󰉋", text: "{{workDirName}}", color: "bright_blue" };
newModule = {
type: "workDir",
icon: "󰉋",
text: "{{workDirName}}",
color: "bright_blue",
};
break;
case "gitBranch":
newModule = { type: "gitBranch", icon: "🌿", text: "{{gitBranch}}", color: "bright_green" };
newModule = {
type: "gitBranch",
icon: "🌿",
text: "{{gitBranch}}",
color: "bright_green",
};
break;
case "model":
newModule = { type: "model", icon: "🤖", text: "{{model}}", color: "bright_yellow" };
newModule = {
type: "model",
icon: "🤖",
text: "{{model}}",
color: "bright_yellow",
};
break;
case "usage":
newModule = { type: "usage", icon: "📊", text: "{{inputTokens}} → {{outputTokens}}", color: "bright_magenta" };
newModule = {
type: "usage",
icon: "📊",
text: "{{inputTokens}} → {{outputTokens}}",
color: "bright_magenta",
};
break;
default:
newModule = { ...DEFAULT_MODULE, type: moduleType };
@@ -448,9 +681,9 @@ const selectedModule = selectedModuleIndex !== null && currentModules.length > s
modules.push(newModule);
setStatusLineConfig(prev => ({
setStatusLineConfig((prev) => ({
...prev,
[currentTheme]: { modules }
[currentTheme]: { modules },
}));
}
}}
@@ -461,8 +694,14 @@ const selectedModule = selectedModuleIndex !== null && currentModules.length > s
<div
key={index}
className={`cursor-pointer ${
selectedModuleIndex === index ? "bg-white/20" : "hover:bg-white/10"
} ${statusLineConfig.currentStyle === 'powerline' ? 'p-0 rounded-none inline-flex overflow-visible relative' : 'flex items-center gap-1 px-2 py-1 rounded'}`}
selectedModuleIndex === index
? "bg-white/20"
: "hover:bg-white/10"
} ${
statusLineConfig.currentStyle === "powerline"
? "p-0 rounded-none inline-flex overflow-visible relative"
: "flex items-center gap-1 px-2 py-1 rounded"
}`}
onClick={() => setSelectedModuleIndex(index)}
draggable
onDragStart={(e) => {
@@ -473,22 +712,39 @@ const selectedModule = selectedModuleIndex !== null && currentModules.length > s
}}
onDrop={(e) => {
e.preventDefault();
const dragIndex = parseInt(e.dataTransfer.getData("dragIndex"));
const dragIndex = parseInt(
e.dataTransfer.getData("dragIndex")
);
if (!isNaN(dragIndex) && dragIndex !== index) {
// 重新排序模块
const currentTheme = statusLineConfig.currentStyle as keyof StatusLineConfig;
const currentTheme =
statusLineConfig.currentStyle as keyof StatusLineConfig;
const themeConfig = statusLineConfig[currentTheme];
const modules = themeConfig && typeof themeConfig === 'object' && 'modules' in themeConfig
? [...((themeConfig as StatusLineThemeConfig).modules || [])]
const modules =
themeConfig &&
typeof themeConfig === "object" &&
"modules" in themeConfig
? [
...((themeConfig as StatusLineThemeConfig)
.modules || []),
]
: [];
if (dragIndex >= 0 && dragIndex < modules.length && index >= 0 && index <= modules.length) {
const [movedModule] = modules.splice(dragIndex, 1);
if (
dragIndex >= 0 &&
dragIndex < modules.length &&
index >= 0 &&
index <= modules.length
) {
const [movedModule] = modules.splice(
dragIndex,
1
);
modules.splice(index, 0, movedModule);
setStatusLineConfig(prev => ({
setStatusLineConfig((prev) => ({
...prev,
[currentTheme]: { modules }
[currentTheme]: { modules },
}));
// 更新选中项的索引
@@ -501,16 +757,30 @@ const selectedModule = selectedModuleIndex !== null && currentModules.length > s
}
}}
>
{renderModulePreview(module, statusLineConfig.currentStyle === 'powerline')}
{renderModulePreview(
module,
statusLineConfig.currentStyle === "powerline"
)}
</div>
))}
</div>
) : (
<div className="flex flex-col items-center justify-center w-full py-4 text-center">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-gray-500 mb-2">
<path d="M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18z"/>
<path d="M12 8v8"/>
<path d="M8 12h8"/>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="text-gray-500 mb-2"
>
<path d="M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18z" />
<path d="M12 8v8" />
<path d="M8 12h8" />
</svg>
<span className="text-gray-500 text-sm">
@@ -531,7 +801,9 @@ const selectedModule = selectedModuleIndex !== null && currentModules.length > s
<Combobox
options={MODULE_TYPES_OPTIONS}
value={selectedModule.type}
onChange={(value) => handleModuleChange(selectedModuleIndex, "type", value)}
onChange={(value) =>
handleModuleChange(selectedModuleIndex, "type", value)
}
/>
<p className="text-xs text-muted-foreground">
@@ -539,12 +811,22 @@ const selectedModule = selectedModuleIndex !== null && currentModules.length > s
</div>
<div className="space-y-2">
<Label htmlFor="module-icon">{t("statusline.module_icon")}</Label>
<Label htmlFor="module-icon">
{t("statusline.module_icon")}
</Label>
<Input
key={fontKey}
id="module-icon"
value={selectedModule.icon || ""}
onChange={(e) => handleModuleChange(selectedModuleIndex, "icon", e.target.value)}
onChange={(e) =>
handleModuleChange(
selectedModuleIndex,
"icon",
e.target.value
)
}
placeholder="例如: 󰉋"
style={fontStyle}
/>
<p className="text-xs text-muted-foreground">
@@ -552,21 +834,54 @@ const selectedModule = selectedModuleIndex !== null && currentModules.length > s
</div>
<div className="space-y-2">
<Label htmlFor="module-text">{t("statusline.module_text")}</Label>
<Label htmlFor="module-text">
{t("statusline.module_text")}
</Label>
<Input
id="module-text"
value={selectedModule.text}
onChange={(e) => handleModuleChange(selectedModuleIndex, "text", e.target.value)}
onChange={(e) =>
handleModuleChange(
selectedModuleIndex,
"text",
e.target.value
)
}
placeholder="例如: {{workDirName}}"
/>
<div className="text-xs text-muted-foreground">
<p>使:</p>
<div className="flex flex-wrap gap-1 mt-1">
<Badge variant="secondary" className="text-xs py-0.5 px-1.5">{"{{workDirName}}"}</Badge>
<Badge variant="secondary" className="text-xs py-0.5 px-1.5">{"{{gitBranch}}"}</Badge>
<Badge variant="secondary" className="text-xs py-0.5 px-1.5">{"{{model}}"}</Badge>
<Badge variant="secondary" className="text-xs py-0.5 px-1.5">{"{{inputTokens}}"}</Badge>
<Badge variant="secondary" className="text-xs py-0.5 px-1.5">{"{{outputTokens}}"}</Badge>
<Badge
variant="secondary"
className="text-xs py-0.5 px-1.5"
>
{"{{workDirName}}"}
</Badge>
<Badge
variant="secondary"
className="text-xs py-0.5 px-1.5"
>
{"{{gitBranch}}"}
</Badge>
<Badge
variant="secondary"
className="text-xs py-0.5 px-1.5"
>
{"{{model}}"}
</Badge>
<Badge
variant="secondary"
className="text-xs py-0.5 px-1.5"
>
{"{{inputTokens}}"}
</Badge>
<Badge
variant="secondary"
className="text-xs py-0.5 px-1.5"
>
{"{{outputTokens}}"}
</Badge>
</div>
</div>
</div>
@@ -575,7 +890,13 @@ const selectedModule = selectedModuleIndex !== null && currentModules.length > s
<Label>{t("statusline.module_color")}</Label>
<ColorPicker
value={selectedModule.color || ""}
onChange={(value) => handleModuleChange(selectedModuleIndex, "color", value)}
onChange={(value) =>
handleModuleChange(
selectedModuleIndex,
"color",
value
)
}
/>
<p className="text-xs text-muted-foreground">
@@ -586,7 +907,13 @@ const selectedModule = selectedModuleIndex !== null && currentModules.length > s
<Label>{t("statusline.module_background")}</Label>
<ColorPicker
value={selectedModule.background || ""}
onChange={(value) => handleModuleChange(selectedModuleIndex, "background", value)}
onChange={(value) =>
handleModuleChange(
selectedModuleIndex,
"background",
value
)
}
/>
<p className="text-xs text-muted-foreground">
@@ -597,16 +924,23 @@ const selectedModule = selectedModuleIndex !== null && currentModules.length > s
variant="destructive"
size="sm"
onClick={() => {
const currentTheme = statusLineConfig.currentStyle as keyof StatusLineConfig;
const currentTheme =
statusLineConfig.currentStyle as keyof StatusLineConfig;
const themeConfig = statusLineConfig[currentTheme];
const modules = themeConfig && typeof themeConfig === 'object' && 'modules' in themeConfig
? [...((themeConfig as StatusLineThemeConfig).modules || [])]
const modules =
themeConfig &&
typeof themeConfig === "object" &&
"modules" in themeConfig
? [
...((themeConfig as StatusLineThemeConfig)
.modules || []),
]
: [];
modules.splice(selectedModuleIndex, 1);
setStatusLineConfig(prev => ({
setStatusLineConfig((prev) => ({
...prev,
[currentTheme]: { modules }
[currentTheme]: { modules },
}));
setSelectedModuleIndex(null);
@@ -617,7 +951,9 @@ const selectedModule = selectedModuleIndex !== null && currentModules.length > s
</div>
) : (
<div className="flex items-center justify-center h-full min-h-[200px]">
<p className="text-muted-foreground text-sm"></p>
<p className="text-muted-foreground text-sm">
</p>
</div>
)}
</div>

View File

@@ -6,7 +6,6 @@ import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
import { Badge } from "@/components/ui/badge"
interface ColorPickerProps {
value?: string;
@@ -15,42 +14,8 @@ interface ColorPickerProps {
showPreview?: boolean;
}
// 预定义的ANSI颜色映射
const ANSI_COLOR_MAP: Record<string, string> = {
"black": "#000000",
"red": "#ff0000",
"green": "#00ff00",
"yellow": "#ffff00",
"blue": "#0000ff",
"magenta": "#ff00ff",
"cyan": "#00ffff",
"white": "#ffffff",
"bright_black": "#808080",
"bright_red": "#ff8080",
"bright_green": "#80ff80",
"bright_yellow": "#ffff80",
"bright_blue": "#8080ff",
"bright_magenta": "#ff80ff",
"bright_cyan": "#80ffff",
"bright_white": "#ffffff"
}
// 背景颜色映射添加bg_前缀
const ANSI_BG_COLOR_MAP: Record<string, string> = Object.keys(ANSI_COLOR_MAP).reduce((acc, key) => {
acc[`bg_${key}`] = ANSI_COLOR_MAP[key]
return acc
}, {} as Record<string, string>)
// 合并所有颜色映射
const ALL_COLOR_MAP = { ...ANSI_COLOR_MAP, ...ANSI_BG_COLOR_MAP }
// 获取颜色值的函数
const getColorValue = (color: string): string => {
// 如果是预定义的ANSI颜色
if (ALL_COLOR_MAP[color]) {
return ALL_COLOR_MAP[color]
}
// 如果是十六进制颜色
if (color.startsWith("#")) {
return color
@@ -91,16 +56,9 @@ export function ColorPicker({
}
}
const handlePresetColorClick = (colorName: string) => {
handleColorChange(colorName)
setOpen(false)
}
const selectedColorValue = getColorValue(value)
// 获取ANSI颜色名称如果适用
const ansiColorName = Object.keys(ALL_COLOR_MAP).find(key => ALL_COLOR_MAP[key] === selectedColorValue) || value
return (
<div className="space-y-2">
<Popover open={open} onOpenChange={setOpen}>
@@ -120,7 +78,7 @@ export function ColorPicker({
/>
)}
<span className="truncate flex-1">
{value ? ansiColorName : placeholder}
{value || placeholder}
</span>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="m7 15 5 5 5-5"/>
@@ -152,7 +110,7 @@ export function ColorPicker({
/>
<div className="flex-1 min-w-0">
<div className="text-sm font-medium truncate">
{value ? ansiColorName : "未选择颜色"}
{value || "未选择颜色"}
</div>
{value && value.startsWith("#") && (
<div className="text-xs text-muted-foreground font-mono">
@@ -184,7 +142,12 @@ export function ColorPicker({
/>
<Button
size="sm"
onClick={() => customColor && handleColorChange(customColor)}
onClick={() => {
if (customColor && /^#[0-9A-F]{6}$/i.test(customColor)) {
handleColorChange(customColor)
setOpen(false)
}
}}
disabled={!customColor || !/^#[0-9A-F]{6}$/i.test(customColor)}
>
@@ -194,66 +157,6 @@ export function ColorPicker({
(: #FF0000)
</p>
</div>
{/* 预定义颜色选项 */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<label className="text-sm font-medium">ANSI </label>
<span className="text-xs text-muted-foreground"></span>
</div>
<div className="grid grid-cols-8 gap-1">
{Object.entries(ANSI_COLOR_MAP).map(([name, color]) => (
<Button
key={name}
variant={value === name ? "default" : "outline"}
size="sm"
className={cn(
"h-8 w-8 p-0 rounded-full transition-all hover:scale-110",
value === name && "ring-2 ring-offset-2 ring-ring ring-offset-background"
)}
style={{ backgroundColor: value === name ? color : undefined }}
onClick={() => handlePresetColorClick(name)}
title={name}
>
{value === name && (
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
<polyline points="20 6 9 17 4 12"/>
</svg>
)}
</Button>
))}
</div>
</div>
{/* 背景颜色选项 */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<label className="text-sm font-medium"></label>
<span className="text-xs text-muted-foreground"></span>
</div>
<div className="grid grid-cols-8 gap-1">
{Object.entries(ANSI_BG_COLOR_MAP).map(([name, color]) => (
<Button
key={name}
variant={value === name ? "default" : "outline"}
size="sm"
className={cn(
"h-8 w-8 p-0 rounded-full transition-all hover:scale-110",
value === name && "ring-2 ring-offset-2 ring-ring ring-offset-background"
)}
style={{ backgroundColor: value === name ? color : undefined }}
onClick={() => handlePresetColorClick(name)}
title={name}
>
{value === name && (
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
<polyline points="20 6 9 17 4 12"/>
</svg>
)}
</Button>
))}
</div>
</div>
</div>
</PopoverContent>
</Popover>

View File

@@ -44,6 +44,7 @@ export interface StatusLineConfig {
currentStyle: string;
default: StatusLineThemeConfig;
powerline: StatusLineThemeConfig;
fontFamily?: string;
}
export interface Config {