ByeByeCode 状态栏 Token 读取原理深度解析
本文将详细解析 ByeByeCode 项目如何读取和显示 Claude Code 的 Token 使用信息,帮助你理解整个数据流的工作原理。
引言
当你使用 Claude Code 时,状态栏会显示类似 42.5% · 85.2k tokens 的信息。你是否好奇过这些数字是从哪里来的?它们是如何被计算和显示的?
ByeByeCode 是一个为 Claude Code 提供自定义状态栏的 Rust CLI 工具。它通过读取 Claude Code 传递的数据和对话记录文件,实时计算并显示 Token 使用情况。
本文将带你深入源码,理解整个 Token 信息读取的完整流程。
整体架构
┌─────────────┐ stdin (JSON) ┌─────────────┐ stdout ┌─────────────┐
│ Claude Code │ ──────────────────▶ │ ByeByeCode │ ─────────────▶ │ 状态栏 │
└─────────────┘ └─────────────┘ └─────────────┘
│
│ 读取 transcript_path
▼
┌─────────────────┐
│ JSONL 对话文件 │
│ (会话记录) │
└─────────────────┘
│
│ 提取 usage 数据
▼
┌─────────────────┐
│ Token 统计 │
│ input/output/ │
│ cache tokens │
└─────────────────┘
核心流程:
- Claude Code 将 JSON 数据通过 stdin 传递给 ByeByeCode
- JSON 中包含
transcript_path,指向对话记录文件 - ByeByeCode 解析 JSONL 文件,提取 Token 使用数据
- 计算百分比,格式化后输出到 stdout
为什么是 Claude Code 主动传递数据?
你可能会问:为什么不让 ByeByeCode 自己去读取 Claude Code 的数据呢?
这是一个很好的问题,涉及到进程间通信的核心设计决策。
关键原因:数据所有权
Claude Code 传递给 ByeByeCode 的数据(如 model、workspace、transcript_path、cost)只存在于 Claude Code 的进程内存中,外部程序根本无法独立获取这些信息。
┌─────────────────────────────────────────────────────────────────┐
│ Claude Code 进程内存 │
├─────────────────────────────────────────────────────────────────┤
│ model.id = "claude-opus-4-5" ← 只存在于内存中 │
│ model.display_name = "Opus 4.5" ← 没有对外暴露的文件 │
│ workspace.current_dir = "/home/..." ← 实时运行状态 │
│ transcript_path = "~/.claude/..." ← 当前会话路径 │
│ cost.total_cost_usd = 0.05 ← 累计费用统计 │
└─────────────────────────────────────────────────────────────────┘
│
│ 只能通过 IPC 传递
▼
┌─────────────────────────────────────────────────────────────────┐
│ ByeByeCode 进程 │
│ │
│ 无法直接访问 Claude Code 的内存空间 │
│ 无法知道当前使用的是哪个模型 │
│ 无法知道当前工作目录 │
│ 无法知道对话记录文件的位置 │
└─────────────────────────────────────────────────────────────────┘
进程间通信流程
Claude Code 使用经典的 fork-exec-pipe 模式来调用 ByeByeCode:
Claude Code 进程(父进程)
│
├─ 1. 准备 JSON 数据
│ 从进程内存中获取:
│ - model.id, model.display_name
│ - workspace.current_dir
│ - transcript_path(当前会话文件路径)
│ - cost 统计数据
│
├─ 2. fork() 创建子进程
│
├─ 3. exec() 执行 byebyecode 命令
│
├─ 4. 通过管道将 JSON 写入子进程的 stdin
│ ┌──────────────────────────────────┐
│ │ {"model":{"id":"..."},...} │
│ └──────────────────────────────────┘
│
└─ 5. 读取子进程的 stdout 作为状态栏内容
┌──────────────────────────────────┐
│ ◉ 42.5% · 85.2k tokens │
└──────────────────────────────────┘
│
▼
ByeByeCode 进程(子进程)
│
├─ 从 stdin 读取 JSON
├─ 解析数据、读取 JSONL 文件
├─ 渲染状态栏字符串
└─ 输出到 stdout → 父进程捕获
两种方案对比
| 对比维度 | stdin 传递(当前方案) | 自行读取(假设方案) |
|---|---|---|
| 数据完整性 | ✅ 完整(Claude Code 提供所有数据) | ❌ 不完整(无法获取内存中的运行时状态) |
| 实时性 | ✅ 每次都是最新数据 | ❌ 可能读到过期文件 |
| 复杂度 | ✅ 低(简单的管道通信) | ❌ 高(需要轮询文件、处理竞态条件) |
| 耦合度 | ✅ 低(只依赖 JSON 格式) | ❌ 高(依赖 Claude Code 内部文件结构) |
| 可靠性 | ✅ 高(同步调用) | ❌ 低(文件可能被其他进程修改) |
Unix 管道哲学
这种设计遵循了 Unix 的核心理念:
“Write programs that do one thing and do it well. Write programs to work together. Write programs to handle text streams, because that is a universal interface.” — Doug McIlroy
- Claude Code:负责收集数据、管理会话(数据生产者)
- ByeByeCode:负责格式化显示(数据消费者)
- stdin/stdout:通用的文本流接口
这种 生产者-消费者模式 使得两个程序完全解耦,各自专注于自己的职责。
数据来源详解
Claude Code 如何调用 ByeByeCode
Claude Code 通过配置 settings.json 中的 statusLine 字段来调用外部程序:
{
"statusLine": {
"enabled": true,
"command": "byebyecode"
}
}
当 Claude Code 需要更新状态栏时,它会启动 byebyecode 进程,并通过 stdin 传入 JSON 数据。
stdin 传入的 JSON 数据结构
让我们看看 Claude Code 传入的数据结构:
// 文件: src/config/types.rs (第 117-123 行)
#[derive(Deserialize)]
pub struct InputData {
pub model: Model, // 当前使用的模型信息
pub workspace: Workspace, // 工作区信息
pub transcript_path: String, // 对话记录文件路径 ← 关键!
pub cost: Option
<Cost>, // 费用信息
pub output_style: Option
<OutputStyle>, // 输出样式
}
关键字段说明:
| 字段 | 类型 | 说明 |
|---|---|---|
model |
Model | 包含模型 ID 和显示名称 |
workspace |
Workspace | 包含当前工作目录 |
transcript_path |
String | 对话记录文件的完整路径 |
cost |
Option | |
| 费用统计(可选) |
stdin 读取代码
// 文件: src/main.rs (第 162-174 行)
// 从 stdin 读取 Claude Code 传入的 JSON 数据
let stdin = io::stdin();
let input: InputData = serde_json::from_reader(stdin.lock())?;
// 收集所有段落的数据
let segments_data = collect_all_segments(&config, &input);
// 渲染状态栏
let generator = StatusLineGenerator::new(config);
let statusline = generator.generate(segments_data);
// 输出到 stdout(Claude Code 会读取并显示)
println!("{}", statusline);
JSONL 对话文件解析
文件位置
transcript_path 指向的文件通常位于:
~/.claude/projects/<project-hash>/sessions/<session-uuid>.jsonl
文件格式
这是一个 JSONL(JSON Lines)格式的文件,每行是一个独立的 JSON 对象,记录了对话的每一轮交互:
{"type":"user","message":{"content":"你好"},"uuid":"abc-123"}
{"type":"assistant","message":{"content":"你好!","usage":{"input_tokens":100,"output_tokens":50}},"uuid":"def-456"}
{"type":"user","message":{"content":"帮我写代码"},"uuid":"ghi-789","parentUuid":"def-456"}
{"type":"assistant","message":{"content":"好的...","usage":{"input_tokens":500,"output_tokens":200}},"uuid":"jkl-012"}
TranscriptEntry 数据结构
// 文件: src/config/types.rs (第 409-419 行)
#[derive(Deserialize)]
pub struct TranscriptEntry {
pub r#type: Option
<String>, // 消息类型: "user" | "assistant" | "summary"
pub message: Option
<Message>, // 消息内容(包含 usage 数据)
pub leaf_uuid: Option
<String>, // 叶子节点 UUID(用于 summary 类型)
pub uuid: Option
<String>, // 消息 UUID
pub parent_uuid: Option
<String>, // 父消息 UUID
pub summary: Option
<String>, // 摘要内容
}
#[derive(Deserialize)]
pub struct Message {
pub usage: Option
<Usage>, // Token 使用统计 ← 这里包含我们需要的数据!
}
Token 数据结构
RawUsage:原始数据格式
由于需要兼容不同的 LLM 提供商(Anthropic、OpenAI 等),RawUsage 结构设计得非常灵活:
// 文件: src/config/types.rs (第 135-185 行)
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct RawUsage {
// ========== Anthropic 风格 ==========
pub input_tokens: Option
<u32>, // 输入 token 数
pub output_tokens: Option
<u32>, // 输出 token 数
pub cache_creation_input_tokens: Option
<u32>, // 缓存创建的 token
pub cache_read_input_tokens: Option
<u32>, // 从缓存读取的 token
// ========== OpenAI 风格 ==========
pub prompt_tokens: Option
<u32>, // 提示词 token(对应 input_tokens)
pub completion_tokens: Option
<u32>, // 完成 token(对应 output_tokens)
pub total_tokens: Option
<u32>, // 总 token 数
pub cached_tokens: Option
<u32>, // 缓存 token
// OpenAI 嵌套详情
pub prompt_tokens_details: Option
<PromptTokensDetails>,
// 未知字段(向前兼容)
#[serde(flatten)]
pub extra: HashMap<String, serde_json::Value>,
}
为什么需要这么多字段?
不同的 API 返回格式不同:
| 提供商 | 输入字段 | 输出字段 | 缓存字段 |
|---|---|---|---|
| Anthropic | input_tokens |
output_tokens |
cache_read_input_tokens |
| OpenAI | prompt_tokens |
completion_tokens |
cached_tokens |
NormalizedUsage:标准化格式
为了统一处理,需要将不同格式标准化:
// 文件: src/config/types.rs (第 188-243 行)
#[derive(Debug, Clone, Serialize, Default, PartialEq)]
pub struct NormalizedUsage {
pub input_tokens: u32, // 标准化后的输入 token
pub output_tokens: u32, // 标准化后的输出 token
pub total_tokens: u32, // 总 token 数
pub cache_creation_input_tokens: u32, // 缓存创建 token
pub cache_read_input_tokens: u32, // 缓存读取 token
pub calculation_source: String, // 计算来源(调试用)
pub raw_data_available: Vec
<String>, // 原始数据可用字段
}
三种 Token 计算方法
impl NormalizedUsage {
/// 上下文窗口使用的 token(用于显示百分比)
/// 包含:输入 + 缓存创建 + 缓存读取 + 输出
pub fn context_tokens(&self) -> u32 {
self.input_tokens
+ self.cache_creation_input_tokens
+ self.cache_read_input_tokens
+ self.output_tokens
}
/// 用于费用计算的总 token
pub fn total_for_cost(&self) -> u32 {
if self.total_tokens > 0 {
self.total_tokens
} else {
self.input_tokens + self.output_tokens
+ self.cache_creation_input_tokens
+ self.cache_read_input_tokens
}
}
/// 用于显示的 token 数(智能选择)
pub fn display_tokens(&self) -> u32 {
let context = self.context_tokens();
if context > 0 { return context; }
if self.total_tokens > 0 { return self.total_tokens; }
self.input_tokens.max(self.output_tokens) // 兜底方案
}
}
核心解析逻辑
parse_transcript_usage 函数
这是解析 Token 数据的入口函数:
// 文件: src/core/segments/context_window.rs (第 86-102 行)
fn parse_transcript_usage<P: AsRef<Path>>(transcript_path: P) -> Option<u32> {
let path = transcript_path.as_ref();
// 尝试从当前对话文件解析
if let Some(usage) = try_parse_transcript_file(path) {
return Some(usage);
}
// 如果文件不存在,尝试从项目历史中查找
if !path.exists() {
if let Some(usage) = try_find_usage_from_project_history(path) {
return Some(usage);
}
}
None
}
try_parse_transcript_file:核心解析函数
// 文件: src/core/segments/context_window.rs (第 104-148 行)
fn try_parse_transcript_file(path: &Path) -> Option
<u32> {
// 1. 打开并读取文件
let file = fs::File::open(path).ok()?;
let reader = BufReader::new(file);
let lines: Vec
<String> = reader.lines()
.collect::<Result<Vec<_>, _>>()
.unwrap_or_default();
if lines.is_empty() {
return None;
}
// 2. 检查最后一行是否是 summary(摘要)类型
let last_line = lines.last()?.trim();
if let Ok(entry) = serde_json::from_str::
<TranscriptEntry>(last_line) {
if entry.r#type.as_deref() == Some("summary") {
// 如果是摘要,通过 leafUuid 查找原始消息
if let Some(leaf_uuid) = &entry.leaf_uuid {
let project_dir = path.parent()?;
return find_usage_by_leaf_uuid(leaf_uuid, project_dir);
}
}
}
// 3. 正常情况:从后往前找最后一条 assistant 消息
for line in lines.iter().rev() {
let line = line.trim();
if line.is_empty() { continue; }
if let Ok(entry) = serde_json::from_str::
<TranscriptEntry>(line) {
// 只处理 assistant 类型的消息
if entry.r#type.as_deref() == Some("assistant") {
if let Some(message) = &entry.message {
if let Some(raw_usage) = &message.usage {
// 4. 标准化 token 数据并返回
let normalized = raw_usage.clone().normalize();
return Some(normalized.display_tokens());
}
}
}
}
}
None
}
解析流程图:
┌─────────────────────────────────────────┐
│ 读取 JSONL 文件 │
└─────────────────┬───────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ 检查最后一行是否是 "summary" 类型 │
└─────────────────┬───────────────────────┘
│
┌────────┴────────┐
│ │
是 summary 不是 summary
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────────┐
│ 通过 leafUuid │ │ 从后往前遍历 │
│ 查找原始消息 │ │ 找 "assistant" 消息 │
└────────┬────────┘ └──────────┬──────────┘
│ │
└──────────┬───────────┘
│
▼
┌─────────────────────┐
│ 提取 message.usage │
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ raw_usage.normalize()│
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ display_tokens() │
│ 返回 token 数量 │
└─────────────────────┘
normalize:Token 数据标准化
// 文件: src/config/types.rs (第 322-398 行)
impl RawUsage {
pub fn normalize(self) -> NormalizedUsage {
let mut result = NormalizedUsage::default();
// 合并输入 token(优先级:Anthropic > OpenAI)
let input = self.input_tokens
.or(self.prompt_tokens)
.unwrap_or(0);
// 合并输出 token
let output = self.output_tokens
.or(self.completion_tokens)
.unwrap_or(0);
// 合并缓存创建 token
let cache_creation = self.cache_creation_input_tokens
.or(self.cache_creation_prompt_tokens)
.unwrap_or(0);
// 合并缓存读取 token(多种来源)
let cache_read = self.cache_read_input_tokens
.or(self.cache_read_prompt_tokens)
.or(self.cached_tokens)
.or_else(|| {
// 回退到 OpenAI 嵌套格式
self.prompt_tokens_details
.as_ref()
.and_then(|d| d.cached_tokens)
})
.unwrap_or(0);
// 计算总数
let total = self.total_tokens.unwrap_or_else(|| {
input + output + cache_read + cache_creation
});
result.input_tokens = input;
result.output_tokens = output;
result.total_tokens = total;
result.cache_creation_input_tokens = cache_creation;
result.cache_read_input_tokens = cache_read;
result
}
}
状态栏渲染
ContextWindowSegment:计算上下文使用率
// 文件: src/core/segments/context_window.rs (第 23-78 行)
impl Segment for ContextWindowSegment {
fn collect(&self, input: &InputData) -> Option
<SegmentData> {
// 1. 获取模型的上下文限制(如 200k)
let context_limit = Self::get_context_limit_for_model(&input.model.id);
// 2. 解析 token 使用量
let context_used_token_opt = parse_transcript_usage(&input.transcript_path);
// 3. 计算显示格式
let (percentage_display, tokens_display) = match context_used_token_opt {
Some(context_used_token) => {
// 计算使用百分比
let context_used_rate =
(context_used_token as f64 / context_limit as f64) * 100.0;
// 格式化百分比:整数不带小数点,否则保留一位
let percentage = if context_used_rate.fract() == 0.0 {
format!("{:.0}%", context_used_rate)
} else {
format!("{:.1}%", context_used_rate)
};
// 格式化 token 数:超过 1000 用 k 表示
let tokens = if context_used_token >= 1000 {
let k_value = context_used_token as f64 / 1000.0;
if k_value.fract() == 0.0 {
format!("{}k", k_value as u32)
} else {
format!("{:.1}k", k_value)
}
} else {
context_used_token.to_string()
};
(percentage, tokens)
}
None => ("-".to_string(), "-".to_string()),
};
// 4. 返回段落数据
Some(SegmentData {
primary: format!("{} · {} tokens", percentage_display, tokens_display),
secondary: String::new(),
metadata: /* ... */,
})
}
}
显示格式示例:
| Token 数量 | 上下文限制 | 显示结果 |
|---|---|---|
| 85,200 | 200,000 | 42.6% · 85.2k tokens |
| 150,000 | 200,000 | 75% · 150k tokens |
| 500 | 200,000 | 0.3% · 500 tokens |
StatusLineGenerator:最终渲染
// 文件: src/core/statusline.rs (第 217-283 行)
fn render_segment(&self, config: &SegmentConfig, data: &SegmentData) -> String {
// 1. 获取图标
let icon = self.get_icon(config); // 如 "◉" 或 Nerd Font 图标
// 2. 应用颜色
let icon_colored = self.apply_color(&icon, config.colors.icon.as_ref());
let text_styled = self.apply_style(
&data.primary, // "42.6% · 85.2k tokens"
config.colors.text.as_ref(),
config.styles.text_bold,
);
// 3. 组合输出
format!("{} {}", icon_colored, text_styled)
// 输出类似:"\x1b[38;5;208m◉\x1b[0m \x1b[37m42.6% · 85.2k tokens\x1b[0m"
}
完整数据流程图
┌──────────────────────────────────────────────────────────────────────────────┐
│ 完整数据流程 │
└──────────────────────────────────────────────────────────────────────────────┘
1. Claude Code 启动 ByeByeCode 进程
┌─────────────────────────────────────────────────────────────────────────┐
│ $ byebyecode │
│ │
│ stdin ◄── JSON 数据: │
│ { │
│ "model": {"id": "claude-3-7-sonnet", "display_name": "Claude 3.7"}, │
│ "workspace": {"current_dir": "/home/user/project"}, │
│ "transcript_path": "~/.claude/projects/.../sessions/xxx.jsonl" │
│ } │
└─────────────────────────────────────────────────────────────────────────┘
│
▼
2. 反序列化 JSON 到 InputData
┌─────────────────────────────────────────────────────────────────────────┐
│ let input: InputData = serde_json::from_reader(stdin.lock())?; │
└─────────────────────────────────────────────────────────────────────────┘
│
▼
3. 读取 transcript_path 指向的 JSONL 文件
┌─────────────────────────────────────────────────────────────────────────┐
│ ~/.claude/projects/.../sessions/xxx.jsonl: │
│ │
│ {"type":"user","message":{...},"uuid":"a1"} │
│ {"type":"assistant","message":{"usage":{"input_tokens":100,...}},...} │
│ {"type":"user","message":{...},"uuid":"b2"} │
│ {"type":"assistant","message":{"usage":{"input_tokens":500,...}},...} │ ◄── 找这条!
└─────────────────────────────────────────────────────────────────────────┘
│
▼
4. 从最后一条 assistant 消息提取 RawUsage
┌─────────────────────────────────────────────────────────────────────────┐
│ RawUsage { │
│ input_tokens: Some(500), │
│ output_tokens: Some(200), │
│ cache_read_input_tokens: Some(1000), │
│ ... │
│ } │
└─────────────────────────────────────────────────────────────────────────┘
│
▼
5. 标准化为 NormalizedUsage
┌─────────────────────────────────────────────────────────────────────────┐
│ let normalized = raw_usage.normalize(); │
│ │
│ NormalizedUsage { │
│ input_tokens: 500, │
│ output_tokens: 200, │
│ cache_read_input_tokens: 1000, │
│ total_tokens: 1700, │
│ } │
└─────────────────────────────────────────────────────────────────────────┘
│
▼
6. 计算显示 token 数
┌─────────────────────────────────────────────────────────────────────────┐
│ normalized.display_tokens() │
│ → context_tokens() = 500 + 0 + 1000 + 200 = 1700 │
└─────────────────────────────────────────────────────────────────────────┘
│
▼
7. ContextWindowSegment 计算百分比
┌─────────────────────────────────────────────────────────────────────────┐
│ context_limit = 200000 (根据模型确定) │
│ percentage = (1700 / 200000) * 100 = 0.85% │
│ display = "0.9% · 1.7k tokens" │
└─────────────────────────────────────────────────────────────────────────┘
│
▼
8. StatusLineGenerator 渲染带颜色的输出
┌─────────────────────────────────────────────────────────────────────────┐
│ stdout ──▶ "\x1b[38;5;208m◉\x1b[0m \x1b[37m0.9% · 1.7k tokens\x1b[0m" │
│ │
│ 显示效果:◉ 0.9% · 1.7k tokens │
└─────────────────────────────────────────────────────────────────────────┘
│
▼
9. Claude Code 读取 stdout 并显示在状态栏
┌─────────────────────────────────────────────────────────────────────────┐
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ byebyecode │ ◆ main ● │ ◉ 0.9% · 1.7k tokens │ opus-4-5 │ │
│ └───────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
总结
核心要点
-
数据来源:Claude Code 通过 stdin 传递 JSON,其中
transcript_path指向对话记录文件 -
文件格式:对话记录是 JSONL 格式,每条 assistant 消息的
message.usage包含 token 统计 -
多格式兼容:
RawUsage设计支持 Anthropic 和 OpenAI 两种格式,通过normalize()统一处理 -
智能显示:
display_tokens()根据可用数据智能选择最合适的 token 数进行显示 -
管道通信:整个过程使用 Unix 管道(stdin/stdout)进行进程间通信,简洁高效
关键代码文件
| 文件 | 功能 |
|---|---|
src/main.rs |
入口,stdin 读取 |
src/config/types.rs |
数据结构定义 |
src/core/segments/context_window.rs |
Token 解析逻辑 |
src/core/statusline.rs |
状态栏渲染 |
扩展阅读
如果你想深入了解:
- serde 库如何实现 JSON 反序列化
- ANSI 转义序列如何实现终端颜色
- Rust 错误处理中
Option和?操作符的使用
本文基于 ByeByeCode 源码分析,希望能帮助你理解状态栏 Token 显示的工作原理。