2. 主题令牌与半行背景块
这一篇讲两个最底层、被几乎所有 UI 模块引用的文件:集中式的主题令牌 themeConfig.ts,以及半行背景块用到的两个 glyph 常量 backgroundBlock.ts。它们对应主题计划文档里的 U2(半行背景块原语)和 U3(接入背景块与主题)。
主题令牌:themeConfig.ts
整个文件就是一份冻结的颜色表:
export const geminiDarkTheme = {
colors: {
bodyBackground: '#000000',
foreground: '#FFFFFF',
muted: '#AFAFAF',
accentBlue: '#87AFFF',
accentGreen: '#D7FFD7',
warning: '#FFFFAF',
errorRed: '#FF87AF',
border: '#878787',
messageBackground: '#5F5F5F',
inputBackground: '#5F5F5F'
}
} as const;
每个令牌的语义
设计上把颜色拆成“前景文本色”和“背景块色”两类,这样即使关闭背景渲染,前景色依然可靠:
bodyBackground#000000:主屏整体底色,也是半行块“未被填充的那一半”的颜色。foreground#FFFFFF:正文主文本、输入框文本、滚动条 thumb。muted#AFAFAF:次要信息,比如底部/ commands | @ mention | ? help提示、空行占位。accentBlue#87AFFF:强调色,用在KQodelogo、assistant 行的•marker、输入框首行的>前缀。accentGreen#D7FFD7:成功态(success类型正文)和右下角模型名GPT-5.5。warning#FFFFAF:pending态正文(如... (pending))。errorRed#FF87AF:前端校验失败与后端错误信息,配合ERROR:文本前缀使用。border#878787:滚动条 track(未被 thumb 覆盖的部分)。messageBackground/inputBackground#5F5F5F:用户消息块、输入框块的背景色。两者目前同值,但分成两个令牌,方便以后单独调。
两个设计要点
第一,as const 让这张表变成只读字面量类型,每个颜色键都是精确的字符串字面量类型,引用处既能自动补全又能在拼错 key 时报错。
第二,主题是内部静态的,没有用户可见的主题配置。主题计划文档明确把 /theme 命令、主题持久化、自定义主题文件等都划在范围之外——当前阶段只需要一份语义清晰、可被集中引用的令牌表。这也是为什么颜色没有散落在各组件里,而是全部从这一个文件导入。
这些颜色取自 Gemini CLI 默认深色配色:消息/输入背景用
#5F5F5F,主屏底色用#000000。
半行背景块:backgroundBlock.ts
文件只有两行,却是整套“无缝背景块”渲染的关键:
export const LOWER_HALF_BLOCK = '▄';
export const UPPER_HALF_BLOCK = '▀';
▄(U+2584,下半块)和 ▀(U+2580,上半块)是两个 Unicode 半块字符。它们之所以能消除终端里的“行缝”,靠的是一个渲染技巧。
为什么需要半行块
主题计划文档记录了一个踩坑:直接给整行刷背景色,会在某些终端的相邻行之间露出可见的“接缝”(行间像素没有被填满)。Gemini CLI 的 HalfLinePaddedBox 给出的思路是——不要让背景块的边缘正好落在“整行”的边界上,而是用半块字符把边缘“柔化”掉。
glyph 的着色技巧
关键在于:一个半块字符会用前景色画它“实心的那一半”,剩下一半显示为背景色。所以一行 ▄(下半块),如果设置 color = 块色、backgroundColor = 底色,渲染出来就是“上半行是底色、下半行是块色”。
把它用在块的顶部,就得到一条只填了下半部分的过渡行;同理 ▀(上半块)用在块的底部,得到只填上半部分的过渡行。中间夹着若干整行(整行都是块背景色)的内容行,最终肉眼看到的是一个上下边缘各“缩进半行”的色块,行与行之间不再有突兀的接缝。
这个技巧在两个地方被复用:
-
正文里的用户消息块——见第 7 篇
bodyRows.ts的halfLineRow():function halfLineRow(columns: number, glyph: string): BodyRow {return {backgroundColor: geminiDarkTheme.colors.bodyBackground,color: geminiDarkTheme.colors.messageBackground,text: glyph.repeat(columns)};}注意
backgroundColor是底色、color是消息块色,正好对应上面说的“实心半边用前景色、另半边用背景色”。 -
输入框的上下边缘——见第 11 篇
ComposerFrame.tsx的ComposerHalfLine:function ComposerHalfLine({ glyph, columns }: { glyph: string; columns: number }) {return (<TextbackgroundColor={geminiDarkTheme.colors.bodyBackground}color={geminiDarkTheme.colors.inputBackground}>{glyph.repeat(Math.max(1, columns))}</Text>);}
从组件到常量的演变
在第一个 commit dd15b678 里,半行块是一个独立的 BackgroundBlock.tsx 组件,带 enabled fallback 等逻辑。重构 commit 5432e018 发现:真正需要复用的只是“上/下半块”这两个 glyph,而“怎么把它们拼成块”在正文和输入框里各有不同(正文是 BodyRow 数据、输入框是 JSX)。于是把组件砍掉,只保留两个常量,由各自的渲染处直接拼接。这既符合“纯 helper 放 libs/tui”的约定,也避免了一个为两种场景强行抽象的中间组件。
下一篇进入状态层,看 homeScreenAtoms.ts 如何用 Jotai 管理主屏的配置与派生状态。